From 0cdb4b7c33e3a2c74498915fb412b53586884ada Mon Sep 17 00:00:00 2001 From: Bernard Assan Date: Mon, 10 Nov 2025 17:46:10 +0000 Subject: [PATCH 1/2] Update zzz to Zig 0.15.2 Fix building Zzz on Zig 0.15.2 Update to use Tardy and Secsock that compiles on 0.15.2 Fix Gzip compression Update Zzz to use std.Io.Writer Signed-off-by: Bernard Assan Update CI to use Zig 0.15.2 Fix example builds Update example in docs to 0.15.2 Add hints on how to run fs and unix examples Use Result location types where possible initialize array types with @splat Use a 8KiB buffer as max http header size by default **TODO** - use a server config option to configure the maximum http header size Signed-off-by: Bernard Assan --- .github/workflows/build.yml | 16 ++-- README.md | 6 +- build.zig | 26 +++--- build.zig.zon | 11 ++- docs/getting_started.md | 15 ++-- docs/https.md | 20 +++-- examples/basic/main.zig | 20 ++--- examples/cookies/main.zig | 22 ++--- examples/form/main.zig | 20 ++--- examples/fs/main.zig | 26 +++--- examples/middleware/main.zig | 20 ++--- examples/sse/main.zig | 22 ++--- examples/tls/main.zig | 27 +++---- examples/unix/main.zig | 22 ++--- src/core/any_case_string_map.zig | 7 +- src/core/pseudoslice.zig | 16 ++-- src/core/typed_storage.zig | 9 +-- src/core/wrapping.zig | 3 +- src/http/context.zig | 16 ++-- src/http/cookie.zig | 26 +++--- src/http/date.zig | 51 ++++++------ src/http/form.zig | 13 ++- src/http/lib.zig | 32 +++----- src/http/method.zig | 4 +- src/http/middlewares/compression.zig | 27 ++++--- src/http/middlewares/lib.zig | 1 - src/http/middlewares/rate_limit.zig | 6 +- src/http/mime.zig | 11 ++- src/http/request.zig | 32 ++++---- src/http/response.zig | 15 ++-- src/http/router.zig | 26 +++--- src/http/router/fs_dir.zig | 39 +++++---- src/http/router/middleware.zig | 14 ++-- src/http/router/route.zig | 15 ++-- src/http/router/routing_trie.zig | 71 ++++++++-------- src/http/server.zig | 116 ++++++++++++++------------- src/http/sse.zig | 40 ++++----- src/lib.zig | 5 +- 38 files changed, 438 insertions(+), 430 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df34d85..8be365f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,11 +19,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: goto-bus-stop/setup-zig@v2 + - uses: actions/checkout@v5 + - uses: mlugg/setup-zig@v2 with: - version: 0.14.0 - - name: Build all examples + version: 0.15.2 + - name: Build all examples run: zig build run-tests: @@ -35,9 +35,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: goto-bus-stop/setup-zig@v2 + - uses: actions/checkout@v5 + - uses: mlugg/setup-zig@v2 with: - version: 0.14.0 - - name: Build all examples + version: 0.15.2 + - name: Build all examples run: zig build test --summary all diff --git a/README.md b/README.md index 0f6bb36..a8bea64 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Installing -Compatible Zig Version: `0.14.0` +Compatible Zig Version: `0.15.2` Compatible [tardy](https://github.com/tardy-org/tardy) Version: `v0.3.0` @@ -62,8 +62,8 @@ zzz can be configured to utilize minimal memory while remaining performant. The - `poll` for Linux, Mac and Windows. - Layered Router, including Middleware - Single and Multithreaded Support -- TLS using [secsock](https://github.com/tardy-org/secsock) -- Memory Pooling for minimal allocations +- TLS using [secsock](https://github.com/tardy-org/secsock) +- Memory Pooling for minimal allocations ## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in zzz by you, shall be licensed as MPL2.0, without any additional terms or conditions. diff --git a/build.zig b/build.zig index 037a9eb..a0af588 100644 --- a/build.zig +++ b/build.zig @@ -38,7 +38,11 @@ pub fn build(b: *std.Build) void { const tests = b.addTest(.{ .name = "tests", - .root_source_file = b.path("./src/tests.zig"), + .root_module = b.addModule("tests", .{ + .root_source_file = b.path("./src/tests.zig"), + .target = target, + .optimize = optimize, + }), }); tests.root_module.addImport("tardy", tardy); tests.root_module.addImport("secsock", secsock); @@ -55,22 +59,24 @@ fn add_example( name: []const u8, link_libc: bool, target: std.Build.ResolvedTarget, - optimize: std.builtin.Mode, + optimize: std.builtin.OptimizeMode, zzz_module: *std.Build.Module, ) void { - const example = b.addExecutable(.{ - .name = name, + const mod = b.createModule(.{ .root_source_file = b.path(b.fmt("./examples/{s}/main.zig", .{name})), - .target = target, .optimize = optimize, + .target = target, .strip = false, + .link_libc = link_libc, }); + mod.addImport("zzz", zzz_module); - if (link_libc) { - example.linkLibC(); - } - - example.root_module.addImport("zzz", zzz_module); + const example = b.addExecutable(.{ + .name = name, + .root_module = mod, + // without llvm leads to error: undefined symbol: tardy_swap_frame + .use_llvm = true, + }); const install_artifact = b.addInstallArtifact(example, .{}); b.getInstallStep().dependOn(&install_artifact.step); diff --git a/build.zig.zon b/build.zig.zon index 2200eb4..6971249 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,18 +2,17 @@ .name = .zzz, .fingerprint = 0xc3273dca261a7ae0, .version = "0.3.0", - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.15.2", .dependencies = .{ .tardy = .{ - .url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de", - .hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6", + .url = "git+https://github.com/bernardassan/tardy?ref=zig-0.15.2#edd54c2dbdb745760848b083b7f844b05d531148", + .hash = "tardy-0.3.0-69wrgn77AwDLFqiryrDCuNl-q7xF9VEUdONc7ytrNvsM", }, .secsock = .{ - .url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24", - .hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr", + .url = "git+https://github.com/bernardassan/secsock?ref=zig-0.15.1#25cec3e1b68dac92c17f3071caacff6b71c00b68", + .hash = "secsock-0.0.0-p0qurQhGAQBqG1dPh8s4htvzl1w8qiGBCxUV9uTOrt9h", }, }, - .paths = .{ "README.md", "LICENSE", diff --git a/docs/getting_started.md b/docs/getting_started.md index 35673cb..8fc3469 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -39,19 +39,22 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), }, .{}); defer router.deinit(allocator); - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + // create socket for tardy + var socket: Socket = try .init(.{ + .tcp = .{ .host = host, .port = port }, + }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -65,7 +68,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, .keepalive_count_max = null, @@ -78,4 +81,4 @@ pub fn main() !void { } ``` -The snippet above handles all of the basic tasks involved with serving a plaintext route using zzz's HTTP implementation. +The snippet above handles all of the basic tasks involved with serving a plaintext route using zzz's HTTP implementation. diff --git a/docs/https.md b/docs/https.md index 6dca483..4812060 100644 --- a/docs/https.md +++ b/docs/https.md @@ -25,6 +25,7 @@ const Respond = http.Respond; const secsock = zzz.secsock; const SecureSocket = secsock.SecureSocket; +const Compression = http.Middlewares.Compression; fn root_handler(ctx: *const Context, _: void) !Respond { const body = @@ -50,25 +51,32 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .{ + .backing_allocator = std.heap.smp_allocator, + }; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, root_handler).layer(), + Compression(.{ .gzip = .{} }), + Route.init("/embed/pico.min.css").embed_file( + .{ .mime = .CSS }, + @embedFile("embed/pico.min.css"), + ).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(1024); - var bearssl = secsock.BearSSL.init(allocator); + var bearssl: secsock.BearSSL = .init(allocator); defer bearssl.deinit(); try bearssl.add_cert_chain( "CERTIFICATE", @@ -87,7 +95,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = secure }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ .stack_size = 1024 * 1024 * 8 }); + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 8 }); try server.serve(rt, p.router, .{ .secure = p.socket }); } }.entry, diff --git a/examples/basic/main.zig b/examples/basic/main.zig index ed7274c..9a2b008 100644 --- a/examples/basic/main.zig +++ b/examples/basic/main.zig @@ -1,20 +1,20 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/basic"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; const Route = http.Route; const Respond = http.Respond; +const log = std.log.scoped(.@"examples/basic"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { return ctx.response.apply(.{ .status = .OK, @@ -27,20 +27,22 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ + .tcp = .{ .host = host, .port = port }, + }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -54,7 +56,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, .keepalive_count_max = null, diff --git a/examples/cookies/main.zig b/examples/cookies/main.zig index 27d633b..7bd6b95 100644 --- a/examples/cookies/main.zig +++ b/examples/cookies/main.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/cookies"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,14 +13,18 @@ const Middleware = http.Middleware; const Respond = http.Respond; const Cookie = http.Cookie; +const log = std.log.scoped(.@"examples/cookies"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { var iter = ctx.request.cookies.iterator(); while (iter.next()) |kv| log.debug("cookie: k={s} v={s}", .{ kv.key_ptr.*, kv.value_ptr.* }); - const cookie = Cookie.init("example_cookie", "abcdef123"); + const cookie: Cookie = .init("example_cookie", "abcdef123"); return ctx.response.apply(.{ .status = .OK, - .mime = http.Mime.HTML, + .mime = .HTML, .body = "Hello, world!", .headers = &.{ .{ "Set-Cookie", try cookie.to_string_alloc(ctx.allocator) }, @@ -36,20 +36,20 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .single }); + var t: Tardy = try .init(allocator, .{ .threading = .single }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -63,7 +63,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, .keepalive_count_max = null, diff --git a/examples/form/main.zig b/examples/form/main.zig index 2cb122f..eeb9f9d 100644 --- a/examples/form/main.zig +++ b/examples/form/main.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/form"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,6 +13,10 @@ const Form = http.Form; const Query = http.Query; const Respond = http.Respond; +const log = std.log.scoped(.@"examples/form"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { const body = \\
@@ -30,7 +30,7 @@ fn base_handler(ctx: *const Context, _: void) !Respond { \\

\\ \\ - \\
+ \\ ; return ctx.response.apply(.{ @@ -80,20 +80,20 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), Route.init("/generate").get({}, generate_handler).post({}, generate_handler).layer(), }, .{}); defer router.deinit(allocator); - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -107,7 +107,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, }); diff --git a/examples/fs/main.zig b/examples/fs/main.zig index d566ebb..80f4b58 100644 --- a/examples/fs/main.zig +++ b/examples/fs/main.zig @@ -1,24 +1,23 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/fs"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; const Dir = tardy.Dir; - const Server = http.Server; const Router = http.Router; const Context = http.Context; const Route = http.Route; const Respond = http.Respond; const FsDir = http.FsDir; - const Compression = http.Middlewares.Compression; +const log = std.log.scoped(.@"examples/fs"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { const body = \\ @@ -36,22 +35,23 @@ fn base_handler(ctx: *const Context, _: void) !Respond { }); } +// Test With: http://localhost:9862/index.html pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator( - .{ .thread_safe = true }, - ){ .backing_allocator = std.heap.c_allocator }; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .{ + .backing_allocator = std.heap.smp_allocator, + }; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples/fs/static", .{})); + const static_dir: Dir = .from_std(try std.fs.cwd().openDir("examples/fs/static", .{})); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Compression(.{ .gzip = .{} }), Route.init("/").get({}, base_handler).layer(), FsDir.serve("/", static_dir), @@ -63,7 +63,7 @@ pub fn main() !void { socket: Socket, }; - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(256); @@ -72,7 +72,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 4, }); diff --git a/examples/middleware/main.zig b/examples/middleware/main.zig index 1efa7e1..a84cca2 100644 --- a/examples/middleware/main.zig +++ b/examples/middleware/main.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/middleware"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,6 +13,10 @@ const Next = http.Next; const Respond = http.Respond; const Middleware = http.Middleware; +const log = std.log.scoped(.@"examples/middleware"); + +const Tardy = tardy.Tardy(.auto); + fn root_handler(ctx: *const Context, id: i8) !Respond { const body_fmt = \\ @@ -39,7 +39,7 @@ fn root_handler(ctx: *const Context, id: i8) !Respond { // client and then continue to await more requests. return ctx.response.apply(.{ .status = .OK, - .mime = http.Mime.HTML, + .mime = .HTML, .body = body[0..], }); } @@ -59,16 +59,16 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .single }); + var t: Tardy = try .init(allocator, .{ .threading = .single }); defer t.deinit(); const num: i8 = 12; - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Middleware.init({}, passing_middleware).layer(), Route.init("/").get(num, root_handler).layer(), Middleware.init({}, failing_middleware).layer(), @@ -82,7 +82,7 @@ pub fn main() !void { socket: Socket, }; - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(256); @@ -91,7 +91,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{}); + var server: Server = .init(.{}); try server.serve(rt, p.router, .{ .normal = p.socket }); } }.entry, diff --git a/examples/sse/main.zig b/examples/sse/main.zig index 715dfab..1236ec9 100644 --- a/examples/sse/main.zig +++ b/examples/sse/main.zig @@ -1,15 +1,11 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/sse"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; const Timer = tardy.Timer; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,8 +13,12 @@ const Route = http.Route; const Respond = http.Respond; const SSE = http.SSE; +const log = std.log.scoped(.@"examples/sse"); + +const Tardy = tardy.Tardy(.auto); + fn sse_handler(ctx: *const Context, _: void) !Respond { - var sse = try SSE.init(ctx); + var sse: SSE = try .init(ctx); while (true) { sse.send(.{ .data = "hello from handler!" }) catch break; @@ -32,20 +32,20 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .single }); + var t: Tardy = try .init(allocator, .{ .threading = .single }); defer t.deinit(); - const router = try Router.init(allocator, &.{ - Route.init("/").embed_file(.{ .mime = http.Mime.HTML }, @embedFile("./index.html")).layer(), + const router: Router = try .init(allocator, &.{ + Route.init("/").embed_file(.{ .mime = .HTML }, @embedFile("./index.html")).layer(), Route.init("/stream").get({}, sse_handler).layer(), }, .{}); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(256); @@ -59,7 +59,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, }); diff --git a/examples/tls/main.zig b/examples/tls/main.zig index d8a07e4..e0e969a 100644 --- a/examples/tls/main.zig +++ b/examples/tls/main.zig @@ -1,24 +1,23 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/tls"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Context = http.Context; const Route = http.Route; const Router = http.Router; const Respond = http.Respond; - const secsock = zzz.secsock; const SecureSocket = secsock.SecureSocket; const Compression = http.Middlewares.Compression; +const log = std.log.scoped(.@"examples/tls"); + +const Tardy = tardy.Tardy(.auto); + fn root_handler(ctx: *const Context, _: void) !Respond { const body = \\ @@ -43,32 +42,32 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator( - .{ .thread_safe = true }, - ){ .backing_allocator = std.heap.c_allocator }; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .{ + .backing_allocator = std.heap.smp_allocator, + }; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, root_handler).layer(), Compression(.{ .gzip = .{} }), Route.init("/embed/pico.min.css").embed_file( - .{ .mime = http.Mime.CSS }, + .{ .mime = .CSS }, @embedFile("embed/pico.min.css"), ).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(1024); - var bearssl = secsock.BearSSL.init(allocator); + var bearssl: secsock.BearSSL = .init(allocator); defer bearssl.deinit(); try bearssl.add_cert_chain( "CERTIFICATE", @@ -87,7 +86,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = secure }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ .stack_size = 1024 * 1024 * 8 }); + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 8 }); try server.serve(rt, p.router, .{ .secure = p.socket }); } }.entry, diff --git a/examples/unix/main.zig b/examples/unix/main.zig index 92c9b88..06300ff 100644 --- a/examples/unix/main.zig +++ b/examples/unix/main.zig @@ -1,39 +1,39 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/benchmark"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Context = http.Context; const Route = http.Route; const Router = http.Router; const Respond = http.Respond; +const log = std.log.scoped(.@"examples/benchmark"); + +const Tardy = tardy.Tardy(.auto); pub const std_options: std.Options = .{ .log_level = .err }; pub fn root_handler(ctx: *const Context, _: void) !Respond { return ctx.response.apply(.{ .status = .OK, - .mime = http.Mime.HTML, - .body = "This is an HTTP benchmark", + .mime = .HTML, + .body = "This is an HTTP benchmark\n", }); } +// Test With: curl --unix-socket /tmp/zzz.sock http://localhost/ pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, root_handler).layer(), }, .{}); defer router.deinit(allocator); @@ -43,7 +43,7 @@ pub fn main() !void { socket: Socket, }; - var socket = try Socket.init(.{ .unix = "/tmp/zzz.sock" }); + var socket: Socket = try .init(.{ .unix = "/tmp/zzz.sock" }); defer std.fs.deleteFileAbsolute("/tmp/zzz.sock") catch unreachable; defer socket.close_blocking(); try socket.bind(); @@ -53,7 +53,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{}); + var server: Server = .init(.{}); try server.serve(rt, p.router, .{ .normal = p.socket }); } }.entry, diff --git a/src/core/any_case_string_map.zig b/src/core/any_case_string_map.zig index e138bad..08ef8b2 100644 --- a/src/core/any_case_string_map.zig +++ b/src/core/any_case_string_map.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; const Pool = @import("tardy").Pool; @@ -7,7 +8,7 @@ const AnyCaseStringContext = struct { const Self = @This(); pub fn hash(_: Self, key: []const u8) u64 { - var wyhash = std.hash.Wyhash.init(0); + var wyhash: std.hash.Wyhash = .init(0); for (key) |b| wyhash.update(&.{std.ascii.toLower(b)}); return wyhash.final(); } @@ -21,10 +22,8 @@ const AnyCaseStringContext = struct { pub const AnyCaseStringMap = std.HashMap([]const u8, []const u8, AnyCaseStringContext, 80); -const testing = std.testing; - test "AnyCaseStringMap: Add Stuff" { - var map = AnyCaseStringMap.init(testing.allocator); + var map: AnyCaseStringMap = .init(testing.allocator); defer map.deinit(); try map.put("Content-Length", "100"); diff --git a/src/core/pseudoslice.zig b/src/core/pseudoslice.zig index c4d519b..fd0d66d 100644 --- a/src/core/pseudoslice.zig +++ b/src/core/pseudoslice.zig @@ -1,5 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; + const log = std.log.scoped(.@"zzz/core/pseudoslice"); // The Pseudoslice will basically stitch together two different buffers, using @@ -11,7 +13,7 @@ pub const Pseudoslice = struct { len: usize, pub fn init(first: []const u8, second: []const u8, shared: []u8) Pseudoslice { - return Pseudoslice{ + return .{ .first = first, .second = second, .shared = shared, @@ -56,12 +58,10 @@ pub const Pseudoslice = struct { } }; -const testing = std.testing; - test "Pseudoslice General" { - var buffer = [_]u8{0} ** 1024; + var buffer: [1024]u8 = @splat(0); const value = "hello, my name is muki"; - var pseudo = Pseudoslice.init(value[0..6], value[6..], buffer[0..]); + var pseudo: Pseudoslice = .init(value[0..6], value[6..], buffer[0..]); for (0..pseudo.len) |i| { for (0..i) |j| { @@ -71,9 +71,9 @@ test "Pseudoslice General" { } test "Pseudoslice Empty Second" { - var buffer = [_]u8{0} ** 1024; + var buffer: [1024]u8 = @splat(0); const value = "hello, my name is muki"; - var pseudo = Pseudoslice.init(value[0..], &.{}, buffer[0..]); + var pseudo: Pseudoslice = .init(value[0..], &.{}, buffer[0..]); for (0..pseudo.len) |i| { try testing.expectEqualStrings(value[0..i], pseudo.get(0, i)); @@ -87,7 +87,7 @@ test "Pseudoslice First and Shared Same" { const value = "hello, my name is muki"; std.mem.copyForwards(u8, buffer, value[0..6]); - var pseudo = Pseudoslice.init(buffer[0..6], value[6..], buffer); + var pseudo: Pseudoslice = .init(buffer[0..6], value[6..], buffer); for (0..pseudo.len) |i| { for (0..i) |j| { diff --git a/src/core/typed_storage.zig b/src/core/typed_storage.zig index 5abe3f2..06866fb 100644 --- a/src/core/typed_storage.zig +++ b/src/core/typed_storage.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const testing = std.testing; pub const TypedStorage = struct { arena: std.heap.ArenaAllocator, @@ -6,8 +7,8 @@ pub const TypedStorage = struct { pub fn init(allocator: std.mem.Allocator) TypedStorage { return .{ - .arena = std.heap.ArenaAllocator.init(allocator), - .storage = std.AutoHashMapUnmanaged(u64, *anyopaque){}, + .arena = .init(allocator), + .storage = .empty, }; } @@ -40,10 +41,8 @@ pub const TypedStorage = struct { } }; -const testing = std.testing; - test "TypedStorage: Basic" { - var storage = TypedStorage.init(testing.allocator); + var storage: TypedStorage = .init(testing.allocator); defer storage.deinit(); // Test inserting and getting different types diff --git a/src/core/wrapping.zig b/src/core/wrapping.zig index 78c0607..5464260 100644 --- a/src/core/wrapping.zig +++ b/src/core/wrapping.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; /// Special values for Wrapped types. const Wrapped = enum(usize) { null = 0, true = 1, false = 2, void = 3 }; @@ -114,8 +115,6 @@ pub fn unwrap(comptime T: type, value: anytype) T { }; } -const testing = std.testing; - test "wrap/unwrap - integers" { try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u8, 42))); try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u16, 42))); diff --git a/src/http/context.zig b/src/http/context.zig index 60dee37..923e64d 100644 --- a/src/http/context.zig +++ b/src/http/context.zig @@ -1,23 +1,21 @@ const std = @import("std"); -const Request = @import("request.zig").Request; -const Response = @import("response.zig").Response; -const Runtime = @import("tardy").Runtime; +const Io = std.Io; +const Runtime = @import("tardy").Runtime; const secsock = @import("secsock"); const SecureSocket = secsock.SecureSocket; -const Capture = @import("router/routing_trie.zig").Capture; - -const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; +const Request = @import("request.zig").Request; +const Response = @import("response.zig").Response; +const Capture = @import("router/routing_trie.zig").Capture; /// HTTP Context. Contains all of the various information /// that will persist throughout the lifetime of this Request/Response. pub const Context = struct { allocator: std.mem.Allocator, - /// Not safe to access unless you are manually sending the headers - /// and returning the .responded variant of Respond. - header_buffer: *std.ArrayList(u8), + header_writer: *Io.Writer, runtime: *Runtime, /// The Request that triggered this handler. request: *const Request, diff --git a/src/http/cookie.zig b/src/http/cookie.zig index 352da8f..f4c513f 100644 --- a/src/http/cookie.zig +++ b/src/http/cookie.zig @@ -1,4 +1,7 @@ const std = @import("std"); +const testing = std.testing; +const Io = std.Io; + const Date = @import("date.zig").Date; pub const Cookie = struct { @@ -34,15 +37,14 @@ pub const Cookie = struct { }; pub fn to_string_buf(self: Cookie, buf: []u8) ![]const u8 { - var list = std.ArrayListUnmanaged(u8).initBuffer(buf); - const writer = list.fixedWriter(); + const writer: std.Io.Writer = .fixed(buf); try writer.print("{s}={s}", .{ self.name, self.value }); if (self.domain) |domain| try writer.print("; Domain={s}", .{domain}); if (self.path) |path| try writer.print("; Path={s}", .{path}); if (self.expires) |exp| { try writer.writeAll("; Expires="); - try exp.to_http_date().into_writer(writer); + try exp.to_http_date().into_writer(&writer); } if (self.max_age) |age| try writer.print("; Max-Age={d}", .{age}); if (self.same_site) |same_site| try writer.print( @@ -52,13 +54,13 @@ pub const Cookie = struct { if (self.secure) try writer.writeAll("; Secure"); if (self.http_only) try writer.writeAll("; HttpOnly"); - return list.items; + return writer.buffered(); } pub fn to_string_alloc(self: Cookie, allocator: std.mem.Allocator) ![]const u8 { - var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 128); - errdefer list.deinit(allocator); - const writer = list.writer(allocator); + var aw: Io.Writer.Allocating = try .initCapacity(allocator, 128); + errdefer aw.deinit(); + const writer = &aw.writer; try writer.print("{s}={s}", .{ self.name, self.value }); if (self.domain) |domain| try writer.print("; Domain={s}", .{domain}); @@ -75,7 +77,7 @@ pub const Cookie = struct { if (self.secure) try writer.writeAll("; Secure"); if (self.http_only) try writer.writeAll("; HttpOnly"); - return list.toOwnedSlice(allocator); + return try aw.toOwnedSlice(); } }; @@ -86,7 +88,7 @@ pub const CookieMap = struct { pub fn init(allocator: std.mem.Allocator) CookieMap { return .{ .allocator = allocator, - .map = std.StringHashMap([]const u8).init(allocator), + .map = .init(allocator), }; } @@ -144,10 +146,8 @@ pub const CookieMap = struct { } }; -const testing = std.testing; - test "Cookie: Header Parsing" { - var cookie_map = CookieMap.init(testing.allocator); + var cookie_map: CookieMap = .init(testing.allocator); defer cookie_map.deinit(); try cookie_map.parse_from_header("sessionId=abc123; java=slop"); @@ -156,7 +156,7 @@ test "Cookie: Header Parsing" { } test "Cookie: Response Formatting" { - const cookie = Cookie{ + const cookie: Cookie = .{ .name = "session", .value = "abc123", .path = "/", diff --git a/src/http/date.zig b/src/http/date.zig index cf0f0ef..1a1a26e 100644 --- a/src/http/date.zig +++ b/src/http/date.zig @@ -1,5 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; +const Io = std.Io; const day_names: []const []const u8 = &.{ "Mon", @@ -17,18 +19,18 @@ const Month = struct { }; const months: []const Month = &.{ - Month{ .name = "Jan", .days = 31 }, - Month{ .name = "Feb", .days = 28 }, - Month{ .name = "Mar", .days = 31 }, - Month{ .name = "Apr", .days = 30 }, - Month{ .name = "May", .days = 31 }, - Month{ .name = "Jun", .days = 30 }, - Month{ .name = "Jul", .days = 31 }, - Month{ .name = "Aug", .days = 31 }, - Month{ .name = "Sep", .days = 30 }, - Month{ .name = "Oct", .days = 31 }, - Month{ .name = "Nov", .days = 30 }, - Month{ .name = "Dec", .days = 31 }, + .{ .name = "Jan", .days = 31 }, + .{ .name = "Feb", .days = 28 }, + .{ .name = "Mar", .days = 31 }, + .{ .name = "Apr", .days = 30 }, + .{ .name = "May", .days = 31 }, + .{ .name = "Jun", .days = 30 }, + .{ .name = "Jul", .days = 31 }, + .{ .name = "Aug", .days = 31 }, + .{ .name = "Sep", .days = 30 }, + .{ .name = "Oct", .days = 31 }, + .{ .name = "Nov", .days = 30 }, + .{ .name = "Dec", .days = 31 }, }; pub const Date = struct { @@ -63,8 +65,7 @@ pub const Date = struct { return try std.fmt.allocPrint(allocator, format, date); } - pub fn into_writer(date: HTTPDate, writer: anytype) !void { - assert(std.meta.hasMethod(@TypeOf(writer), "print")); + pub fn into_writer(date: HTTPDate, writer: *Io.Writer) !void { try writer.print(format, date); } }; @@ -72,7 +73,7 @@ pub const Date = struct { ts: i64, pub fn init(ts: i64) Date { - return Date{ .ts = ts }; + return .{ .ts = ts }; } fn is_leap_year(year: i64) bool { @@ -108,7 +109,7 @@ pub const Date = struct { const minute: u8 = @intCast(@mod(@divFloor(remsecs, 60), 60)); const second: u8 = @intCast(@mod(remsecs, 60)); - return HTTPDate{ + return .{ .day_name = day_names[@intCast(week_day)], .day = @intCast(day), .month = months[month].name, @@ -120,19 +121,17 @@ pub const Date = struct { } }; -const testing = std.testing; - test "Parse Basic Date (Buffer)" { const ts = 1727411110; - var date: Date = Date.init(ts); - var buffer = [_]u8{0} ** 29; + var date: Date = .init(ts); + var buffer: [29]u8 = @splat(0); const http_date = date.to_http_date(); try testing.expectEqualStrings("Fri, 27 Sep 2024 04:25:10 GMT", try http_date.into_buf(buffer[0..])); } test "Parse Basic Date (Alloc)" { const ts = 1727464105; - var date: Date = Date.init(ts); + var date: Date = .init(ts); const http_date = date.to_http_date(); const http_string = try http_date.into_alloc(testing.allocator); defer testing.allocator.free(http_string); @@ -141,11 +140,11 @@ test "Parse Basic Date (Alloc)" { test "Parse Basic Date (Writer)" { const ts = 672452112; - var date: Date = Date.init(ts); + var date: Date = .init(ts); const http_date = date.to_http_date(); - var buffer = [_]u8{0} ** 29; - var stream = std.io.fixedBufferStream(buffer[0..]); - try http_date.into_writer(stream.writer()); - const http_string = stream.getWritten(); + var buffer: [29]u8 = @splat(0); + var stream_w: Io.Writer = .fixed(&buffer); + try http_date.into_writer(&stream_w); + const http_string = stream_w.buffered(); try testing.expectEqualStrings("Wed, 24 Apr 1991 00:15:12 GMT", http_string); } diff --git a/src/http/form.zig b/src/http/form.zig index 6cd887e..dc641c3 100644 --- a/src/http/form.zig +++ b/src/http/form.zig @@ -1,11 +1,12 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; const Context = @import("context.zig").Context; pub fn decode_alloc(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { - var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, input.len); + var list: std.ArrayList(u8) = try .initCapacity(allocator, input.len); defer list.deinit(allocator); var input_index: usize = 0; @@ -98,7 +99,7 @@ fn construct_map_from_body(allocator: std.mem.Allocator, m: *AnyCaseStringMap, b pub fn Form(comptime T: type) type { return struct { pub fn parse(allocator: std.mem.Allocator, ctx: *const Context) !T { - var m = AnyCaseStringMap.init(ctx.allocator); + var m: AnyCaseStringMap = .init(ctx.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { @@ -127,14 +128,12 @@ pub fn Query(comptime T: type) type { }; } -const testing = std.testing; - test "FormData: Parsing from Body" { const UserRole = enum { admin, visitor }; const User = struct { id: u32, name: []const u8, age: u8, role: UserRole }; const body: []const u8 = "id=10&name=John&age=12&role=visitor"; - var m = AnyCaseStringMap.init(testing.allocator); + var m: AnyCaseStringMap = .init(testing.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { @@ -158,7 +157,7 @@ test "FormData: Parsing Missing Fields" { const User = struct { id: u32, name: []const u8, age: u8 }; const body: []const u8 = "id=10"; - var m = AnyCaseStringMap.init(testing.allocator); + var m: AnyCaseStringMap = .init(testing.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { @@ -177,7 +176,7 @@ test "FormData: Parsing Missing Fields" { test "FormData: Parsing Missing Value" { const body: []const u8 = "abc=abc&id="; - var m = AnyCaseStringMap.init(testing.allocator); + var m: AnyCaseStringMap = .init(testing.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { diff --git a/src/http/lib.zig b/src/http/lib.zig index 5b2b3f3..148534e 100644 --- a/src/http/lib.zig +++ b/src/http/lib.zig @@ -1,32 +1,26 @@ -pub const Status = @import("status.zig").Status; -pub const Method = @import("method.zig").Method; -pub const Request = @import("request.zig").Request; -pub const Response = @import("response.zig").Response; -pub const Respond = @import("response.zig").Respond; -pub const Mime = @import("mime.zig").Mime; -pub const Encoding = @import("encoding.zig").Encoding; -pub const Date = @import("date.zig").Date; +pub const Context = @import("context.zig").Context; pub const Cookie = @import("cookie.zig").Cookie; - +pub const Date = @import("date.zig").Date; +pub const Encoding = @import("encoding.zig").Encoding; pub const Form = @import("form.zig").Form; +pub const Method = @import("method.zig").Method; +pub const Middlewares = @import("middlewares/lib.zig"); +pub const Mime = @import("mime.zig").Mime; pub const Query = @import("form.zig").Query; - -pub const Context = @import("context.zig").Context; - +pub const Request = @import("request.zig").Request; +pub const Respond = @import("response.zig").Respond; +pub const Response = @import("response.zig").Response; pub const Router = @import("router.zig").Router; -pub const Route = @import("router/route.zig").Route; -pub const SSE = @import("sse.zig").SSE; - +pub const FsDir = @import("router/fs_dir.zig").FsDir; pub const Layer = @import("router/middleware.zig").Layer; pub const Middleware = @import("router/middleware.zig").Middleware; pub const MiddlewareFn = @import("router/middleware.zig").MiddlewareFn; pub const Next = @import("router/middleware.zig").Next; -pub const Middlewares = @import("middlewares/lib.zig"); - -pub const FsDir = @import("router/fs_dir.zig").FsDir; - +pub const Route = @import("router/route.zig").Route; pub const Server = @import("server.zig").Server; pub const ServerConfig = @import("server.zig").ServerConfig; +pub const SSE = @import("sse.zig").SSE; +pub const Status = @import("status.zig").Status; pub const HTTPError = error{ TooManyHeaders, diff --git a/src/http/method.zig b/src/http/method.zig index 8302a03..2743c6e 100644 --- a/src/http/method.zig +++ b/src/http/method.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const testing = std.testing; + const log = std.log.scoped(.@"zzz/http/method"); pub const Method = enum(u8) { @@ -45,8 +47,6 @@ pub const Method = enum(u8) { } }; -const testing = std.testing; - test "Parsing Strings" { for (std.meta.tags(Method)) |method| { const method_string = @tagName(method); diff --git a/src/http/middlewares/compression.zig b/src/http/middlewares/compression.zig index a2e7935..49ec776 100644 --- a/src/http/middlewares/compression.zig +++ b/src/http/middlewares/compression.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const flate = std.compress.flate; const Respond = @import("../response.zig").Respond; const Middleware = @import("../router/middleware.zig").Middleware; @@ -6,9 +7,9 @@ const Next = @import("../router/middleware.zig").Next; const Layer = @import("../router/middleware.zig").Layer; const TypedMiddlewareFn = @import("../router/middleware.zig").TypedMiddlewareFn; -const Kind = union(enum) { - gzip: std.compress.gzip.Options, -}; +const Kind = union(enum) { gzip: struct { + level: flate.Compress.Level = .default, +} }; /// Compression Middleware. /// @@ -16,23 +17,23 @@ const Kind = union(enum) { /// will properly compress the body and add the proper `Content-Encoding` header. pub fn Compression(comptime compression: Kind) Layer { const func: TypedMiddlewareFn(void) = switch (compression) { - .gzip => |inner| struct { + .gzip => |_| struct { fn gzip_mw(next: *Next, _: void) !Respond { const respond = try next.run(); const response = next.context.response; if (response.body) |body| if (respond == .standard) { - var compressed = try std.ArrayListUnmanaged(u8).initCapacity(next.context.allocator, body.len); - errdefer compressed.deinit(next.context.allocator); + var compressed: std.Io.Writer.Allocating = try .initCapacity(next.context.allocator, body.len); + errdefer compressed.deinit(); - var body_stream = std.io.fixedBufferStream(body); - try std.compress.gzip.compress( - body_stream.reader(), - compressed.writer(next.context.allocator), - inner, - ); + var body_stream: flate.Compress = .init(&compressed.writer, &.{}, .{ + .level = compression.gzip.level, + .container = .gzip, + }); + try body_stream.writer.writeAll(body); + try body_stream.writer.flush(); try response.headers.put("Content-Encoding", "gzip"); - response.body = try compressed.toOwnedSlice(next.context.allocator); + response.body = try compressed.toOwnedSlice(); return .standard; }; diff --git a/src/http/middlewares/lib.zig b/src/http/middlewares/lib.zig index c6077f3..8b52164 100644 --- a/src/http/middlewares/lib.zig +++ b/src/http/middlewares/lib.zig @@ -1,4 +1,3 @@ pub const Compression = @import("compression.zig").Compression; - pub const RateLimitConfig = @import("rate_limit.zig").RateLimitConfig; pub const RateLimiting = @import("rate_limit.zig").RateLimiting; diff --git a/src/http/middlewares/rate_limit.zig b/src/http/middlewares/rate_limit.zig index bea12f8..c0286e6 100644 --- a/src/http/middlewares/rate_limit.zig +++ b/src/http/middlewares/rate_limit.zig @@ -53,10 +53,10 @@ pub const RateLimitConfig = struct { max_tokens: u16, response_on_limited: ?Respond, ) RateLimitConfig { - const map = std.AutoHashMap(u128, Bucket).init(allocator); - const respond = response_on_limited orelse Response.Fields{ + const map: std.AutoHashMap(u128, Bucket) = .init(allocator); + const respond = response_on_limited orelse .{ .status = .@"Too Many Requests", - .mime = Mime.TEXT, + .mime = .TEXT, .body = "", }; diff --git a/src/http/mime.zig b/src/http/mime.zig index 34adb56..3dcc85c 100644 --- a/src/http/mime.zig +++ b/src/http/mime.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; const Pair = @import("../core/lib.zig").Pair; @@ -144,7 +145,7 @@ const all_mime_types = blk: { break :blk return_mimes; }; -const mime_extension_map = blk: { +const mime_extension_map: std.StaticStringMap(Mime) = blk: { const num_pairs = num: { var count: usize = 0; for (all_mime_types) |mime| { @@ -177,10 +178,10 @@ const mime_extension_map = blk: { } } - break :blk std.StaticStringMap(Mime).initComptime(pairs); + break :blk .initComptime(pairs); }; -const mime_content_map = blk: { +const mime_content_map: std.StaticStringMap(Mime) = blk: { const num_pairs = num: { var count: usize = 0; for (all_mime_types) |mime| { @@ -213,11 +214,9 @@ const mime_content_map = blk: { } } - break :blk std.StaticStringMap(Mime).initComptime(pairs); + break :blk .initComptime(pairs); }; -const testing = std.testing; - test "MIME from extensions" { for (all_mime_types) |mime| { switch (mime.extension) { diff --git a/src/http/request.zig b/src/http/request.zig index c74e1f0..9fa1ba4 100644 --- a/src/http/request.zig +++ b/src/http/request.zig @@ -1,12 +1,14 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/http/request"); const assert = std.debug.assert; +const testing = std.testing; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; const CookieMap = @import("cookie.zig").CookieMap; const HTTPError = @import("lib.zig").HTTPError; const Method = @import("lib.zig").Method; +const log = std.log.scoped(.@"zzz/http/request"); + pub const Request = struct { allocator: std.mem.Allocator, method: ?Method = null, @@ -18,10 +20,10 @@ pub const Request = struct { /// This is for constructing a Request. pub fn init(allocator: std.mem.Allocator) Request { - const headers = AnyCaseStringMap.init(allocator); - const cookies = CookieMap.init(allocator); + const headers: AnyCaseStringMap = .init(allocator); + const cookies: CookieMap = .init(allocator); - return Request{ + return .{ .allocator = allocator, .headers = headers, .cookies = cookies, @@ -124,8 +126,6 @@ pub const Request = struct { } }; -const testing = std.testing; - test "Parse Request" { const request_text = \\GET / HTTP/1.1 @@ -159,8 +159,9 @@ test "Expect ContentTooLong Error" { \\Accept: text/html ; - const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 4096}); - var request = Request.init(testing.allocator); + const large_content: [4096]u8 = @splat('a'); + const request_text = std.fmt.comptimePrint(request_text_format, .{large_content}); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -178,8 +179,9 @@ test "Expect URITooLong Error" { \\Accept: text/html ; - const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 4096}); - var request = Request.init(testing.allocator); + const large_content: [4096]u8 = @splat('a'); + const request_text = std.fmt.comptimePrint(request_text_format, .{large_content[0..]}); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -196,9 +198,9 @@ test "Expect Malformed when URI missing /" { \\Connection: keep-alive \\Accept: text/html ; - - const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 256}); - var request = Request.init(testing.allocator); + const content: [256]u8 = @splat('a'); + const request_text = std.fmt.comptimePrint(request_text_format, .{content[0..]}); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -216,7 +218,7 @@ test "Expect Incorrect HTTP Version" { \\Accept: text/html ; - var request = Request.init(testing.allocator); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -234,7 +236,7 @@ test "Malformed AnyCaseStringMap" { \\Accept: text/html ; - var request = Request.init(testing.allocator); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ diff --git a/src/http/response.zig b/src/http/response.zig index 5f42a2a..a7a606f 100644 --- a/src/http/response.zig +++ b/src/http/response.zig @@ -1,12 +1,13 @@ const std = @import("std"); const assert = std.debug.assert; +const Io = std.Io; + +const Stream = @import("tardy").Stream; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; -const Status = @import("lib.zig").Status; -const Mime = @import("lib.zig").Mime; const Date = @import("lib.zig").Date; - -const Stream = @import("tardy").Stream; +const Mime = @import("lib.zig").Mime; +const Status = @import("lib.zig").Status; pub const Respond = enum { // When we are returning a real HTTP request, we use this. @@ -31,8 +32,8 @@ pub const Response = struct { }; pub fn init(allocator: std.mem.Allocator) Response { - const headers = AnyCaseStringMap.init(allocator); - return Response{ .headers = headers }; + const headers: AnyCaseStringMap = .init(allocator); + return .{ .headers = headers }; } pub fn deinit(self: *Response) void { @@ -54,7 +55,7 @@ pub const Response = struct { self.headers.clearRetainingCapacity(); } - pub fn headers_into_writer(self: *Response, writer: anytype, content_length: ?usize) !void { + pub fn headers_into_writer(self: *Response, writer: *Io.Writer, content_length: ?usize) !void { // Status Line const status = self.status.?; try writer.print("HTTP/1.1 {d} {s}\r\n", .{ @intFromEnum(status), @tagName(status) }); diff --git a/src/http/router.zig b/src/http/router.zig index 91b79c9..e68caea 100644 --- a/src/http/router.zig +++ b/src/http/router.zig @@ -1,22 +1,20 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/http/router"); const assert = std.debug.assert; +const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const Context = @import("context.zig").Context; +const Mime = @import("mime.zig").Mime; +const Request = @import("request.zig").Request; +const Respond = @import("response.zig").Respond; +const Response = @import("response.zig").Response; const Layer = @import("router/middleware.zig").Layer; const Route = @import("router/route.zig").Route; const TypedHandlerFn = @import("router/route.zig").TypedHandlerFn; - const Bundle = @import("router/routing_trie.zig").Bundle; - const Capture = @import("router/routing_trie.zig").Capture; -const Request = @import("request.zig").Request; -const Response = @import("response.zig").Response; -const Respond = @import("response.zig").Respond; -const Mime = @import("mime.zig").Mime; -const Context = @import("context.zig").Context; - const RoutingTrie = @import("router/routing_trie.zig").RoutingTrie; -const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; + +const log = std.log.scoped(.@"zzz/http/router"); /// Default not found handler: send a plain text response. pub const default_not_found_handler = struct { @@ -45,12 +43,10 @@ pub const Router = struct { layers: []const Layer, configuration: Configuration, ) !Router { - const self = Router{ - .routes = try RoutingTrie.init(allocator, layers), + return .{ + .routes = try .init(allocator, layers), .configuration = configuration, }; - - return self; } pub fn deinit(self: *Router, allocator: std.mem.Allocator) void { @@ -66,7 +62,7 @@ pub const Router = struct { ) !Bundle { queries.clearRetainingCapacity(); - return try self.routes.get_bundle(allocator, path, captures, queries) orelse Bundle{ + return try self.routes.get_bundle(allocator, path, captures, queries) orelse .{ .route = Route.init("").all({}, self.configuration.not_found), .captures = captures[0..], .queries = queries, diff --git a/src/http/router/fs_dir.zig b/src/http/router/fs_dir.zig index b9b8ee5..a6d76b5 100644 --- a/src/http/router/fs_dir.zig +++ b/src/http/router/fs_dir.zig @@ -1,20 +1,20 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/http/router"); const assert = std.debug.assert; -const Route = @import("route.zig").Route; -const Layer = @import("middleware.zig").Layer; -const Request = @import("../request.zig").Request; -const Respond = @import("../response.zig").Respond; -const Mime = @import("../mime.zig").Mime; -const Context = @import("../context.zig").Context; - -const Runtime = @import("tardy").Runtime; -const ZeroCopy = @import("tardy").ZeroCopy; const Dir = @import("tardy").Dir; +const Runtime = @import("tardy").Runtime; const Stat = @import("tardy").Stat; - const Stream = @import("tardy").Stream; +const ZeroCopy = @import("tardy").ZeroCopy; + +const Context = @import("../context.zig").Context; +const Mime = @import("../mime.zig").Mime; +const Request = @import("../request.zig").Request; +const Respond = @import("../response.zig").Respond; +const Layer = @import("middleware.zig").Layer; +const Route = @import("route.zig").Route; + +const log = std.log.scoped(.@"zzz/http/router"); pub const FsDir = struct { fn fs_dir_handler(ctx: *const Context, dir: Dir) !Respond { @@ -45,14 +45,14 @@ pub const FsDir = struct { error.NotFound => { return ctx.response.apply(.{ .status = .@"Not Found", - .mime = Mime.HTML, + .mime = .HTML, }); }, else => return e, }; const stat = try file.stat(ctx.runtime); - var hash = std.hash.Wyhash.init(0); + var hash: std.hash.Wyhash = .init(0); hash.update(std.mem.asBytes(&stat.size)); if (stat.modified) |modified| { hash.update(std.mem.asBytes(&modified.seconds)); @@ -69,7 +69,7 @@ pub const FsDir = struct { // If the ETag matches. return ctx.response.apply(.{ .status = .@"Not Modified", - .mime = Mime.HTML, + .mime = .HTML, }); } } @@ -78,12 +78,16 @@ pub const FsDir = struct { response.status = .OK; response.mime = mime; - try response.headers_into_writer(ctx.header_buffer.writer(), stat.size); - const headers = ctx.header_buffer.items; + response.headers_into_writer(ctx.header_writer, stat.size) catch |err| switch (err) { + error.WriteFailed => return error.ExceededMaxHttpHeaderSize, + else => unreachable, + }; + const headers = ctx.header_writer.buffered(); const length = try ctx.socket.send_all(ctx.runtime, headers); if (headers.len != length) return error.SendingHeadersFailed; - var buffer = ctx.header_buffer.allocatedSlice(); + // Reuse the header_buffer for file read/send + var buffer = ctx.header_writer.buffer[0..]; while (true) { const read_count = file.read(ctx.runtime, buffer, null) catch |e| switch (e) { error.EndOfFile => break, @@ -105,6 +109,7 @@ pub const FsDir = struct { "{s}/%r", .{std.mem.trimRight(u8, url_path, "/")}, ); + log.debug("url with match {s}", .{url_with_match_all}); return Route.init(url_with_match_all).get(dir, fs_dir_handler).layer(); } diff --git a/src/http/router/middleware.zig b/src/http/router/middleware.zig index 191d10c..79e21f0 100644 --- a/src/http/router/middleware.zig +++ b/src/http/router/middleware.zig @@ -1,18 +1,18 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/router/middleware"); const assert = std.debug.assert; const Runtime = @import("tardy").Runtime; -const wrap = @import("../../core/wrapping.zig").wrap; const Pseudoslice = @import("../../core/pseudoslice.zig").Pseudoslice; -const Server = @import("../server.zig").Server; - -const Mime = @import("../mime.zig").Mime; -const Route = @import("route.zig").Route; -const HandlerWithData = @import("route.zig").HandlerWithData; +const wrap = @import("../../core/wrapping.zig").wrap; const Context = @import("../context.zig").Context; +const Mime = @import("../mime.zig").Mime; const Respond = @import("../response.zig").Respond; +const Server = @import("../server.zig").Server; +const HandlerWithData = @import("route.zig").HandlerWithData; +const Route = @import("route.zig").Route; + +const log = std.log.scoped(.@"zzz/router/middleware"); pub const Layer = union(enum) { /// Route diff --git a/src/http/router/route.zig b/src/http/router/route.zig index f29d2c0..5004d08 100644 --- a/src/http/router/route.zig +++ b/src/http/router/route.zig @@ -1,24 +1,23 @@ const std = @import("std"); -const builtin = @import("builtin"); -const log = std.log.scoped(.@"zzz/http/route"); const assert = std.debug.assert; +const builtin = @import("builtin"); const wrap = @import("../../core/wrapping.zig").wrap; - +const Context = @import("../context.zig").Context; +const Encoding = @import("../encoding.zig").Encoding; const Method = @import("../method.zig").Method; +const Mime = @import("../mime.zig").Mime; const Request = @import("../request.zig").Request; const Response = @import("../response.zig").Response; const Respond = @import("../response.zig").Respond; -const Mime = @import("../mime.zig").Mime; -const Encoding = @import("../encoding.zig").Encoding; - const FsDir = @import("fs_dir.zig").FsDir; -const Context = @import("../context.zig").Context; const Layer = @import("middleware.zig").Layer; - const MiddlewareWithData = @import("middleware.zig").MiddlewareWithData; +const log = std.log.scoped(.@"zzz/http/route"); + pub const HandlerFn = *const fn (*const Context, usize) anyerror!Respond; + pub fn TypedHandlerFn(comptime T: type) type { return *const fn (*const Context, T) anyerror!Respond; } diff --git a/src/http/router/routing_trie.zig b/src/http/router/routing_trie.zig index c59ec33..d54495d 100644 --- a/src/http/router/routing_trie.zig +++ b/src/http/router/routing_trie.zig @@ -1,18 +1,17 @@ const std = @import("std"); const assert = std.debug.assert; -const log = std.log.scoped(.@"zzz/http/routing_trie"); - -const Layer = @import("middleware.zig").Layer; -const Route = @import("route.zig").Route; +const testing = std.testing; -const Respond = @import("../response.zig").Respond; +const AnyCaseStringMap = @import("../../core/any_case_string_map.zig").AnyCaseStringMap; +const decode_alloc = @import("../form.zig").decode_alloc; const Context = @import("../lib.zig").Context; - +const Respond = @import("../response.zig").Respond; const HandlerWithData = @import("route.zig").HandlerWithData; +const Layer = @import("middleware.zig").Layer; const MiddlewareWithData = @import("middleware.zig").MiddlewareWithData; -const AnyCaseStringMap = @import("../../core/any_case_string_map.zig").AnyCaseStringMap; +const Route = @import("route.zig").Route; -const decode_alloc = @import("../form.zig").decode_alloc; +const log = std.log.scoped(.@"zzz/http/routing_trie"); fn TokenHashMap(comptime V: type) type { return std.HashMap(Token, V, struct { @@ -136,7 +135,7 @@ pub const RoutingTrie = struct { return .{ .token = token, .route = route, - .children = TokenHashMap(Node).init(allocator), + .children = .init(allocator), }; } @@ -152,13 +151,13 @@ pub const RoutingTrie = struct { }; root: Node, - middlewares: std.ArrayListUnmanaged(MiddlewareWithData), + middlewares: std.ArrayList(MiddlewareWithData), /// Initialize the routing tree with the given routes. pub fn init(allocator: std.mem.Allocator, layers: []const Layer) !Self { var self: Self = .{ - .root = Node.init(allocator, .{ .fragment = "" }, null), - .middlewares = try std.ArrayListUnmanaged(MiddlewareWithData).initCapacity(allocator, 0), + .root = .init(allocator, .{ .fragment = "" }, null), + .middlewares = .empty, }; for (layers) |layer| { @@ -168,7 +167,7 @@ pub const RoutingTrie = struct { var iter = std.mem.tokenizeScalar(u8, route.path, '/'); while (iter.next()) |chunk| { - const token: Token = Token.parse_chunk(chunk); + const token: Token = .parse_chunk(chunk); if (current.children.getPtr(token)) |child| { current = child; } else { @@ -183,7 +182,7 @@ pub const RoutingTrie = struct { }; for (route.handlers, 0..) |handler, i| if (handler) |h| { - r.handlers[i] = HandlerWithData{ + r.handlers[i] = .{ .handler = h.handler, .middlewares = self.middlewares.items, .data = h.data, @@ -229,21 +228,21 @@ pub const RoutingTrie = struct { .match => |kind| { switch (kind) { .signed => if (std.fmt.parseInt(i64, chunk, 10)) |value| { - captures[capture_idx] = Capture{ .signed = value }; + captures[capture_idx] = .{ .signed = value }; } else |_| continue :child_loop, .unsigned => if (std.fmt.parseInt(u64, chunk, 10)) |value| { - captures[capture_idx] = Capture{ .unsigned = value }; + captures[capture_idx] = .{ .unsigned = value }; } else |_| continue :child_loop, // Float types MUST have a '.' to differentiate them. .float => if (std.mem.indexOfScalar(u8, chunk, '.')) |_| { if (std.fmt.parseFloat(f64, chunk)) |value| { - captures[capture_idx] = Capture{ .float = value }; + captures[capture_idx] = .{ .float = value }; } else |_| continue :child_loop; } else continue :child_loop, - .string => captures[capture_idx] = Capture{ .string = chunk }, + .string => captures[capture_idx] = .{ .string = chunk }, .remaining => { const rest = iter.buffer[(iter.index - chunk.len)..]; - captures[capture_idx] = Capture{ .remaining = rest }; + captures[capture_idx] = .{ .remaining = rest }; current = child; capture_idx += 1; @@ -264,7 +263,7 @@ pub const RoutingTrie = struct { return null; } - var duped = try std.ArrayListUnmanaged([]const u8).initCapacity(allocator, 0); + var duped: std.ArrayList([]const u8) = .empty; defer duped.deinit(allocator); errdefer for (duped.items) |d| allocator.free(d); @@ -293,7 +292,7 @@ pub const RoutingTrie = struct { } } - return Bundle{ + return .{ .route = current.route orelse return null, .captures = captures[0..capture_idx], .queries = queries, @@ -302,11 +301,9 @@ pub const RoutingTrie = struct { } }; -const testing = std.testing; - test "Chunk Parsing (Fragment)" { const chunk = "thisIsAFragment"; - const token: Token = Token.parse_chunk(chunk); + const token: Token = .parse_chunk(chunk); switch (token) { .fragment => |inner| try testing.expectEqualStrings(chunk, inner), @@ -323,7 +320,7 @@ test "Chunk Parsing (Match)" { "%s", }; - const matches = [_]TokenMatch{ + const matches: [5]TokenMatch = .{ TokenMatch.signed, TokenMatch.signed, TokenMatch.unsigned, @@ -332,7 +329,7 @@ test "Chunk Parsing (Match)" { }; for (chunks, matches) |chunk, match| { - const token: Token = Token.parse_chunk(chunk); + const token: Token = .parse_chunk(chunk); switch (token) { .fragment => return error.IncorrectTokenParsing, @@ -353,7 +350,7 @@ test "Path Parsing (Mixed)" { var iter = std.mem.tokenizeScalar(u8, path, '/'); for (parsed) |expected| { - const token = Token.parse_chunk(iter.next().?); + const token: Token = .parse_chunk(iter.next().?); switch (token) { .fragment => |inner| try testing.expectEqualStrings(expected.fragment, inner), .match => |inner| try testing.expectEqual(expected.match, inner), @@ -362,7 +359,7 @@ test "Path Parsing (Mixed)" { } test "Constructing Routing from Path" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%i/description").layer(), Route.init("/item/%i/hello").layer(), @@ -376,7 +373,7 @@ test "Constructing Routing from Path" { } test "Routing with Paths" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%i/description").layer(), Route.init("/item/%i/hello").layer(), @@ -386,10 +383,10 @@ test "Routing with Paths" { }); defer s.deinit(testing.allocator); - var q = AnyCaseStringMap.init(testing.allocator); + var q: AnyCaseStringMap = .init(testing.allocator); defer q.deinit(); - var captures: [8]Capture = [_]Capture{undefined} ** 8; + var captures: [8]Capture = @splat(undefined); try testing.expectEqual(null, try s.get_bundle(testing.allocator, "/item/name", captures[0..], &q)); @@ -409,7 +406,7 @@ test "Routing with Paths" { } test "Routing with Remaining" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%f/price_float").layer(), Route.init("/item/name/%r").layer(), @@ -417,10 +414,10 @@ test "Routing with Remaining" { }); defer s.deinit(testing.allocator); - var q = AnyCaseStringMap.init(testing.allocator); + var q: AnyCaseStringMap = .init(testing.allocator); defer q.deinit(); - var captures: [8]Capture = [_]Capture{undefined} ** 8; + var captures: [8]Capture = @splat(undefined); try testing.expectEqual(null, try s.get_bundle(testing.allocator, "/item/name", captures[0..], &q)); @@ -465,7 +462,7 @@ test "Routing with Remaining" { } test "Routing with Queries" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%f/price_float").layer(), Route.init("/item/name/%r").layer(), @@ -473,10 +470,10 @@ test "Routing with Queries" { }); defer s.deinit(testing.allocator); - var q = AnyCaseStringMap.init(testing.allocator); + var q: AnyCaseStringMap = .init(testing.allocator); defer q.deinit(); - var captures: [8]Capture = [_]Capture{undefined} ** 8; + var captures: [8]Capture = @splat(undefined); try testing.expectEqual(null, try s.get_bundle( testing.allocator, diff --git a/src/http/server.zig b/src/http/server.zig index d3432db..329f190 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -1,47 +1,42 @@ const std = @import("std"); +const assert = std.debug.assert; +const Io = std.Io; const builtin = @import("builtin"); const tag = builtin.os.tag; -const assert = std.debug.assert; -const log = std.log.scoped(.@"zzz/http/server"); -const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; -const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; -const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const AcceptResult = @import("tardy").AcceptResult; +const Cross = @import("tardy").Cross; +const Pool = @import("tardy").Pool; +const PoolKind = @import("tardy").PoolKind; +const RecvResult = @import("tardy").RecvResult; +pub const Runtime = @import("tardy").Runtime; +const secsock = @import("secsock"); +const SecureSocket = secsock.SecureSocket; +const SendResult = @import("tardy").SendResult; +const Socket = @import("tardy").Socket; +const TardyCreator = @import("tardy").Tardy; +pub const Task = @import("tardy").Task; +const ZeroCopy = @import("tardy").ZeroCopy; +const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; +const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; const Context = @import("context.zig").Context; +const HTTPError = @import("lib.zig").HTTPError; +const Mime = @import("mime.zig").Mime; const Request = @import("request.zig").Request; -const Response = @import("response.zig").Response; const Respond = @import("response.zig").Respond; -const Capture = @import("router/routing_trie.zig").Capture; -const SSE = @import("sse.zig").SSE; - -const Mime = @import("mime.zig").Mime; +const Response = @import("response.zig").Response; const Router = @import("router.zig").Router; -const Route = @import("router/route.zig").Route; const Layer = @import("router/middleware.zig").Layer; const Middleware = @import("router/middleware.zig").Middleware; -const HTTPError = @import("lib.zig").HTTPError; - -const HandlerWithData = @import("router/route.zig").HandlerWithData; - const Next = @import("router/middleware.zig").Next; +const Route = @import("router/route.zig").Route; +const HandlerWithData = @import("router/route.zig").HandlerWithData; +const Capture = @import("router/routing_trie.zig").Capture; +const SSE = @import("sse.zig").SSE; -pub const Runtime = @import("tardy").Runtime; -pub const Task = @import("tardy").Task; -const TardyCreator = @import("tardy").Tardy; - -const Cross = @import("tardy").Cross; -const Pool = @import("tardy").Pool; -const PoolKind = @import("tardy").PoolKind; -const Socket = @import("tardy").Socket; -const ZeroCopy = @import("tardy").ZeroCopy; - -const AcceptResult = @import("tardy").AcceptResult; -const RecvResult = @import("tardy").RecvResult; -const SendResult = @import("tardy").SendResult; - -const secsock = @import("secsock"); -const SecureSocket = secsock.SecureSocket; +const log = std.log.scoped(.@"zzz/http/server"); pub const TLSFileOptions = union(enum) { buffer: []const u8, @@ -128,7 +123,7 @@ pub const Provision = struct { initalized: bool = false, recv_slice: []u8, zc_recv_buffer: ZeroCopy(u8), - header_buffer: std.ArrayList(u8), + header_writer: Io.Writer, arena: std.heap.ArenaAllocator, storage: TypedStorage, captures: []Capture, @@ -173,7 +168,7 @@ pub const Server = struct { provision.response.clear(); provision.storage.clear(); provision.zc_recv_buffer.clear_retaining_capacity(); - provision.header_buffer.clearRetainingCapacity(); + _ = provision.header_writer.consumeAll(); _ = provision.arena.reset(.{ .retain_with_limit = config.connection_arena_bytes_retain }); provision.recv_slice = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); @@ -235,15 +230,16 @@ pub const Server = struct { provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch { @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)"); }; - provision.header_buffer = std.ArrayList(u8).init(rt.allocator); - provision.arena = std.heap.ArenaAllocator.init(rt.allocator); + provision.arena = .init(rt.allocator); + // TODO: use a server config option + provision.header_writer = .fixed(try provision.arena.allocator().alloc(u8, 8 * 1024)); provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch { @panic("attempting to allocate more memory than available. (Captures)"); }; - provision.queries = AnyCaseStringMap.init(rt.allocator); - provision.storage = TypedStorage.init(rt.allocator); - provision.request = Request.init(rt.allocator); - provision.response = Response.init(rt.allocator); + provision.queries = .init(rt.allocator); + provision.storage = .init(rt.allocator); + provision.request = .init(rt.allocator); + provision.response = .init(rt.allocator); provision.initalized = true; } defer prepare_new_request(null, provision, config) catch unreachable; @@ -287,7 +283,7 @@ pub const Server = struct { }, ); - log.info("rt{d} - \"{s} {s}\" {s} ({})", .{ + log.info("rt{d} - \"{s} {s}\" {s} ({f})", .{ rt.id, @tagName(provision.request.method.?), provision.request.uri.?, @@ -361,7 +357,7 @@ pub const Server = struct { const context: Context = .{ .runtime = rt, .allocator = provision.arena.allocator(), - .header_buffer = &provision.header_buffer, + .header_writer = &provision.header_writer, .request = &provision.request, .response = &provision.response, .storage = &provision.storage, @@ -377,7 +373,7 @@ pub const Server = struct { }; const next_respond: Respond = next.run() catch |e| blk: { - log.warn("rt{d} - \"{s} {s}\" {} ({})", .{ + log.warn("rt{d} - \"{s} {s}\" {} ({f})", .{ rt.id, @tagName(provision.request.method.?), provision.request.uri.?, @@ -391,7 +387,7 @@ pub const Server = struct { break :blk try provision.response.apply(.{ .status = .@"Internal Server Error", - .mime = Mime.TEXT, + .mime = .TEXT, .body = body, }); }; @@ -423,11 +419,11 @@ pub const Server = struct { const body = provision.response.body orelse ""; const content_length = body.len; - try provision.response.headers_into_writer(provision.header_buffer.writer(), content_length); - const headers = provision.header_buffer.items; + try provision.response.headers_into_writer(&provision.header_writer, content_length); + const headers = provision.header_writer.buffered(); var sent: usize = 0; - const pseudo = Pseudoslice.init(headers, body, provision.recv_slice); + const pseudo: Pseudoslice = .init(headers, body, provision.recv_slice); while (sent < pseudo.len) { const send_slice = pseudo.get(sent, sent + provision.recv_slice.len); @@ -455,7 +451,7 @@ pub const Server = struct { }, }; - log.info("connection ({}) closed", .{secure.socket.addr}); + log.info("connection ({f}) closed", .{secure.socket.addr}); if (!accept_queued.*) { try rt.spawn( @@ -477,7 +473,7 @@ pub const Server = struct { log.info("security mode: {s}", .{@tagName(sock)}); const secure: SecureSocket = switch (sock) { - .normal => |s| SecureSocket.unsecured(s), + .normal => |s| .unsecured(s), .secure => |sec| sec, }; @@ -485,7 +481,7 @@ pub const Server = struct { const pooling: PoolKind = if (self.config.connection_count_max == null) .grow else .static; const provision_pool = try rt.allocator.create(Pool(Provision)); - provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling); + provision_pool.* = try .init(rt.allocator, count, pooling); errdefer rt.allocator.destroy(provision_pool); const connection_count = try rt.allocator.create(usize); @@ -496,6 +492,14 @@ pub const Server = struct { errdefer rt.allocator.destroy(accept_queued); accept_queued.* = true; + // Use a Max Header Size of 8KiB same as Nginx, Tomcat and Httpd but + // consider making this configurable + // https://stackoverflow.com/questions/686217/maximum-on-http-header-values + const max_http_header_size = 1024 * 8; + const pool_header_buffer: []u8 = try rt.allocator.alloc(u8, count * max_http_header_size); + errdefer rt.allocator.free(pool_header_buffer); + var next_header_buffer_index: usize = 0; + // initialize first batch of provisions :) for (provision_pool.items) |*provision| { provision.initalized = true; @@ -505,15 +509,17 @@ pub const Server = struct { ) catch { @panic("attempting to allocate more memory than available. (ZeroCopy)"); }; - provision.header_buffer = std.ArrayList(u8).init(rt.allocator); - provision.arena = std.heap.ArenaAllocator.init(rt.allocator); + provision.header_writer = .fixed(pool_header_buffer[next_header_buffer_index..][0..max_http_header_size]); + next_header_buffer_index += max_http_header_size; + + provision.arena = .init(rt.allocator); provision.captures = rt.allocator.alloc(Capture, self.config.capture_count_max) catch { @panic("attempting to allocate more memory than available. (Captures)"); }; - provision.queries = AnyCaseStringMap.init(rt.allocator); - provision.storage = TypedStorage.init(rt.allocator); - provision.request = Request.init(rt.allocator); - provision.response = Response.init(rt.allocator); + provision.queries = .init(rt.allocator); + provision.storage = .init(rt.allocator); + provision.request = .init(rt.allocator); + provision.response = .init(rt.allocator); } try rt.spawn( diff --git a/src/http/sse.zig b/src/http/sse.zig index d0a6b5d..44c54e1 100644 --- a/src/http/sse.zig +++ b/src/http/sse.zig @@ -1,15 +1,16 @@ const std = @import("std"); +const Writer = std.Io.Writer; -const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; +const Runtime = @import("tardy").Runtime; +const secsock = @import("secsock"); +const SecureSocket = secsock.SecureSocket; -const Provision = @import("server.zig").Provision; +const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; const Context = @import("context.zig").Context; const Mime = @import("mime.zig").Mime; +const Provision = @import("server.zig").Provision; -const Runtime = @import("tardy").Runtime; - -const secsock = @import("secsock"); -const SecureSocket = secsock.SecureSocket; +const log = std.log.scoped(.@"zzz/http/sse"); const SSEMessage = struct { id: ?[]const u8 = null, @@ -20,40 +21,38 @@ const SSEMessage = struct { pub const SSE = struct { socket: SecureSocket, - allocator: std.mem.Allocator, - list: std.ArrayListUnmanaged(u8), + writer: Writer.Allocating, runtime: *Runtime, pub fn init(ctx: *const Context) !SSE { const response = ctx.response; response.status = .OK; - response.mime = Mime{ + response.mime = .{ .content_type = .{ .single = "text/event-stream" }, .extension = .{ .single = "" }, .description = "SSE", }; - var list = try std.ArrayListUnmanaged(u8).initCapacity(ctx.allocator, 0); - errdefer list.deinit(ctx.allocator); + var writer: Writer.Allocating = .init(ctx.allocator); + errdefer writer.deinit(); - try ctx.response.headers_into_writer(ctx.header_buffer.writer(), null); - const headers = ctx.header_buffer.items; + try ctx.response.headers_into_writer(ctx.header_writer, null); + const headers = ctx.header_writer.buffered(); const sent = try ctx.socket.send_all(ctx.runtime, headers); if (sent != headers.len) return error.Closed; return .{ .socket = ctx.socket, - .allocator = ctx.allocator, - .list = list, + .writer = writer, .runtime = ctx.runtime, }; } pub fn send(self: *SSE, message: SSEMessage) !void { - // just reuse the list - defer self.list.clearRetainingCapacity(); - const writer = self.list.writer(self.allocator); + var aw = &self.writer; + defer aw.clearRetainingCapacity(); // reuse the writer + const writer = &aw.writer; if (message.id) |id| try writer.print("id: {s}\n", .{id}); if (message.event) |event| try writer.print("event: {s}\n", .{event}); @@ -64,7 +63,8 @@ pub const SSE = struct { if (message.retry) |retry| try writer.print("retry: {d}\n", .{retry}); try writer.writeByte('\n'); - const sent = try self.socket.send_all(self.runtime, self.list.items); - if (sent != self.list.items.len) return error.Closed; + const written = aw.written(); + const sent = try self.socket.send_all(self.runtime, written); + if (sent != written.len) return error.Closed; } }; diff --git a/src/lib.zig b/src/lib.zig index 96d9dd7..ca7256c 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,10 +1,9 @@ const std = @import("std"); -/// Internally exposed Tardy. -pub const tardy = @import("tardy"); - /// Internally exposed secsock. pub const secsock = @import("secsock"); +/// Internally exposed Tardy. +pub const tardy = @import("tardy"); /// HyperText Transfer Protocol. /// Supports: HTTP/1.1 From c6e186a1aca2ca0db5709af160d43c24c2bd4809 Mon Sep 17 00:00:00 2001 From: Bernard Assan Date: Thu, 16 Apr 2026 10:11:31 +0000 Subject: [PATCH 2/2] Update tardy and secsock dependencies add zig-pkg/ to .gitignore Signed-off-by: Bernard Assan --- .gitignore | 1 + build.zig.zon | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2c30843..fe4c407 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ zig-out/ +zig-pkg/ .zig-cache/ perf*.data* heaptrack* diff --git a/build.zig.zon b/build.zig.zon index 6971249..8405556 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,12 +5,12 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .tardy = .{ - .url = "git+https://github.com/bernardassan/tardy?ref=zig-0.15.2#edd54c2dbdb745760848b083b7f844b05d531148", + .url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#26cbc3cab7993b04e6447471eb523b21376d7dee", .hash = "tardy-0.3.0-69wrgn77AwDLFqiryrDCuNl-q7xF9VEUdONc7ytrNvsM", }, .secsock = .{ - .url = "git+https://github.com/bernardassan/secsock?ref=zig-0.15.1#25cec3e1b68dac92c17f3071caacff6b71c00b68", - .hash = "secsock-0.0.0-p0qurQhGAQBqG1dPh8s4htvzl1w8qiGBCxUV9uTOrt9h", + .url = "git+https://github.com/tardy-org/secsock?ref=0.1.0#0f0cdf8bdbad7c898d09dbf07febfe9d4e2ed6f5", + .hash = "secsock-0.0.0-p0qurTFGAQAfehQXehAc0To-z9uTF2fHw5wMBPM-q9Pk", }, }, .paths = .{