From 922b6e8d0b136d41779a743aa0c0477209e0324a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoffer=20Vikstr=C3=B6m?= Date: Sat, 10 Oct 2026 20:37:46 +0200 Subject: [PATCH] feat: migrate to Zig 0.17 --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 5 +- README.md | 16 +++-- bench/client.zig | 12 ++-- bench/con-buffers.py | 8 ++- bench/credentials.zig | 2 +- bench/protocol.zig | 4 +- build.zig | 4 +- build.zig.zon | 2 +- docs/REMOTE_BENCHMARKS.md | 14 ++-- docs/ROADMAP.md | 2 +- docs/ZIG_017_PERFORMANCE.md | 119 ++++++++++++++++++++++++++++++++++ src/Client.zig | 28 ++++---- src/Io.zig | 45 +++++++++++-- src/Server.zig | 22 +++---- src/block_transfer.zig | 18 ++--- src/constants.zig | 1 - src/deferred.zig | 4 +- src/dtls/Ccm.zig | 106 +++++++++++++++++++----------- src/dtls/Cookie.zig | 38 +++++------ src/dtls/Handshake.zig | 42 ++++++------ src/dtls/Prf.zig | 2 +- src/dtls/Record.zig | 18 ++--- src/dtls/Session.zig | 24 +++---- src/dtls/integration_test.zig | 4 +- src/dtls/types.zig | 4 +- src/handler.zig | 6 +- src/observe.zig | 12 ++-- src/packet_view.zig | 10 +-- src/platform.zig | 15 ++++- src/rate_limiter.zig | 10 +-- src/router.zig | 2 +- 32 files changed, 400 insertions(+), 203 deletions(-) create mode 100644 docs/ZIG_017_PERFORMANCE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15be22f..f37d6cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - uses: mlugg/setup-zig@v2 with: - version: 0.16.0 + version: 0.17.0 - name: Build examples and benchmark tools run: zig build @@ -23,7 +23,7 @@ jobs: run: zig build test - name: Build optimized artifacts - run: zig build -Doptimize=ReleaseFast + run: zig build -Doptimize=fast - name: Remote benchmark integration run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7218c..81f5d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,15 @@ - Bound Block2 reassembly; correlate capacity failures and support local request cancellation - Preserve Block2 tokens/handles, sort continuation options, and handle empty ACK/RST and separate replies - Synchronize DTLS benchmark workers and report common-wall successful-response throughput; add allocator selection -- Require Zig 0.16.0; replace public peer addresses with `std.Io.net.IpAddress` +- Require Zig 0.17.0; replace public peer addresses with `std.Io.net.IpAddress` - Retain the Linux io_uring backend with native-address conversion at syscall boundaries - Use monotonic clocks for protocol deadlines and timing - Update allocator examples, benchmark argument handling, OpenSSL process APIs and CI - Vectorize full-block CCM CBC-MAC XOR; add independent boundary test vectors - Add `--payload-only` benchmark filtering and document migration performance measurements +- Adopt Zig 0.17 array splats, backing-integer builtins, build argument forwarding and optimization names +- Preserve native-endian Linux socket bytes across the Zig 0.17 `@bitCast` change; add socket-layout/decode tests +- Avoid Zig 0.17 CCM array/vector bit-cast scalarization with unaligned byte-vector views; verify ciphertext and auth-failure output on unaligned buffers ## [0.7.2] - 2026-03-21 - Fix: Block1 reassembled body now reaches handler (#81) diff --git a/README.md b/README.md index 4c5e486..2c364fa 100644 --- a/README.md +++ b/README.md @@ -933,7 +933,7 @@ uses shared-memory counters; remote NON uses unique receipt accounting. CON meas round-trip latency. Results vary by hardware — run on your own system: ```bash -zig build bench -Doptimize=ReleaseFast +zig build bench -Doptimize=fast ``` Filter flags: `--plain-only`, `--dtls-only`, `--con-only`, `--non-only`, @@ -944,12 +944,13 @@ server-side counters. `--allocator debug|smp` selects the client allocator (default: DebugAllocator); this is separate from the Zig build mode. DTLS workers synchronize after handshake/warmup and use one common wall interval for successful CON response throughput. See [bounded response storage and -matched benchmarks](docs/CON_BUFFERS.md) and [the Zig 0.16 migration measurements](docs/ZIG_016_PERFORMANCE.md). +matched benchmarks](docs/CON_BUFFERS.md), [the Zig 0.17 migration measurements](docs/ZIG_017_PERFORMANCE.md) +and [the historical Zig 0.16 measurements](docs/ZIG_016_PERFORMANCE.md). For two-machine testing, build the dedicated configurable server: ```sh -zig build bench-server -Doptimize=ReleaseFast +zig build bench-server -Doptimize=fast ./zig-out/bin/bench-server --bind --threads 8 --allocator smp ``` @@ -962,7 +963,14 @@ See [remote benchmark setup and accounting](docs/REMOTE_BENCHMARKS.md). ## Requirements - Linux kernel 6.0+ (multishot recvmsg; shared provided-buffer rings and cooperative task running) -- Zig 0.16.0 (the currently supported toolchain) +- Zig 0.17.0 (the currently supported toolchain) + +### Migrating from Zig 0.16 + +Use optimization names `debug`, `safe`, `fast` and `small` (for example, +`-Doptimize=fast`). Build scripts forward benchmark arguments through +`run_bench.addPassthruArgs()`. The library's public API and io_uring backend +are unchanged by this toolchain migration relative to current `main`. ### Migrating from Zig 0.15 diff --git a/bench/client.zig b/bench/client.zig index cf209ad..0a89532 100644 --- a/bench/client.zig +++ b/bench/client.zig @@ -115,7 +115,7 @@ pub fn main(init: std.process.Init.Minimal) !void { else @intCast(@min(std.Thread.getCpuCount() catch 1, 256)); - var scenarios: [scenario_templates.len]?Scenario = .{null} ** scenario_templates.len; + var scenarios: [scenario_templates.len]?Scenario = @splat(null); var total: u16 = 0; for (scenario_templates, 0..) |tmpl, i| { if (config.payload_filter) |size| { @@ -172,7 +172,7 @@ pub fn main(init: std.process.Init.Minimal) !void { } const counters = try alloc_shared_counters(); - var results: [scenario_templates.len]?ScenarioResult = .{null} ** scenario_templates.len; + var results: [scenario_templates.len]?ScenarioResult = @splat(null); var current_group: ?ServerGroup = null; var server_pid: ?posix.pid_t = null; var scenario_num: u16 = 0; @@ -327,7 +327,7 @@ fn run_scenario_plain_non( const per_thread = count / tc; const remainder = count % tc; - var send_errors: [256]u64 = .{0} ** 256; + var send_errors: [256]u64 = @splat(0); var spawned: usize = 0; errdefer for (threads[0..spawned]) |thread| thread.join(); @@ -960,7 +960,7 @@ fn run_bench( timestamps[token] = 0; consecutive_timeouts = 0; in_flight -= 1; - if (recv_buf[1] != @intFromEnum(coapz.Code.content)) { + if (recv_buf[1] != @backingInt(coapz.Code.content)) { result.errors += 1; continue; } @@ -1145,7 +1145,7 @@ fn percentile_us(sorted: []i64, p: f64) f64 { } fn format_label(template: []const u8, thread_count: u16) [24]u8 { - var buf: [24]u8 = .{' '} ** 24; + var buf: [24]u8 = @splat(' '); var out_i: usize = 0; var in_i: usize = 0; while (in_i < template.len and out_i < buf.len) { @@ -1284,7 +1284,7 @@ fn print_scenario_tree( const l_branch: []const u8 = if (leaf_last) "└─" else "├─"; const tc: u16 = if (s.multi_thread) cpu_count else 1; - var leaf_buf: [12]u8 = .{' '} ** 12; + var leaf_buf: [12]u8 = @splat(' '); _ = std.fmt.bufPrint(&leaf_buf, "{d:>2}T {d:>4}B", .{ tc, s.payload_size }) catch {}; std.debug.print(" " ++ D ++ "{s}{s}{s}" ++ Z ++ " {s}", .{ tr_cont, ty_cont, l_branch, &leaf_buf }); diff --git a/bench/con-buffers.py b/bench/con-buffers.py index adc4126..b1376e9 100644 --- a/bench/con-buffers.py +++ b/bench/con-buffers.py @@ -2,7 +2,8 @@ """Compare response storage using matched common-wall benchmark generators. The control uses the unmodified base library and con-buffers-control.patch on -this revision's benchmark source. Both binaries must be ReleaseFast builds. +this revision's benchmark source. Both binaries must be fast builds +(ReleaseFast on Zig 0.16). """ import argparse import hashlib @@ -56,7 +57,7 @@ def main(): data = { "machine": platform.platform(), "zig": subprocess.check_output(["zig", "version"], text=True).strip(), - "build_mode": "ReleaseFast", "allocators": args.allocators, + "build_mode": "fast", "allocators": args.allocators, "base_library_commit": "51d6d76dcfc1be0c345602f4eef95991065c8730", "binary_sha256": {key: hashlib.sha256(path.read_bytes()).hexdigest() for key, path in binaries.items()}, "control_patch_sha256": hashlib.sha256(patch.read_bytes()).hexdigest(), @@ -78,7 +79,8 @@ def main(): "--window", "32", "--payload-only", str(payload), "--count", str(args.count), "--warmup", "1000", "--allocator", allocator, "--port", "29680"] output = run(command) - if f"build ReleaseFast; client allocator {allocator};" not in output: + if not any(f"build {mode}; client allocator {allocator};" in output + for mode in ("fast", "ReleaseFast")): raise RuntimeError("Build/allocator mismatch: " + output) matches = list(ROW.finditer(output)) if len(matches) != 1: diff --git a/bench/credentials.zig b/bench/credentials.zig index f9c38e6..6f2e8a3 100644 --- a/bench/credentials.zig +++ b/bench/credentials.zig @@ -30,7 +30,7 @@ pub const Credentials = struct { } pub fn load(path: []const u8) !Credentials { var path_buffer: [std.fs.max_path_bytes + 1]u8 = undefined; - const zpath = try std.fmt.bufPrintZ(&path_buffer, "{s}", .{path}); + const zpath = try std.fmt.bufPrintSentinel(&path_buffer, "{s}", .{path}, 0); const fd = try coap.platform.open(zpath, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); defer coap.platform.close(fd); var buffer: [2049]u8 = undefined; diff --git a/bench/protocol.zig b/bench/protocol.zig index aaa78ee..426fe9f 100644 --- a/bench/protocol.zig +++ b/bench/protocol.zig @@ -14,14 +14,14 @@ pub const Start = struct { var bytes: [start_len]u8 = undefined; @memcpy(bytes[0..16], &self.nonce); std.mem.writeInt(u32, bytes[16..20], self.expected, .big); - bytes[20] = @intFromEnum(self.mode); + bytes[20] = @backingInt(self.mode); return bytes; } pub fn decode(bytes: []const u8) !Start { if (bytes.len != start_len) return error.InvalidStart; const count = std.mem.readInt(u32, bytes[16..20], .big); if (count == 0 or count > max_requests or bytes[20] > 1) return error.InvalidStart; - return .{ .nonce = bytes[0..16].*, .expected = count, .mode = @enumFromInt(bytes[20]) }; + return .{ .nonce = bytes[0..16].*, .expected = count, .mode = @fromBackingInt(@intCast(bytes[20])) }; } }; pub const Stats = struct { diff --git a/build.zig b/build.zig index 251111d..57dc0b3 100644 --- a/build.zig +++ b/build.zig @@ -60,9 +60,7 @@ pub fn build(b: *std.Build) void { b.installArtifact(bench_exe); const run_bench = b.addRunArtifact(bench_exe); - if (b.args) |args| { - run_bench.addArgs(args); - } + run_bench.addPassthruArgs(); const bench_step = b.step("bench", "Run benchmark client"); bench_step.dependOn(&run_bench.step); diff --git a/build.zig.zon b/build.zig.zon index f7c427a..135fc23 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .coap, .version = "0.7.2", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .fingerprint = 0x67bf20367bf50f8b, .dependencies = .{ .coapz = .{ diff --git a/docs/REMOTE_BENCHMARKS.md b/docs/REMOTE_BENCHMARKS.md index 3d0bfd2..b064f9f 100644 --- a/docs/REMOTE_BENCHMARKS.md +++ b/docs/REMOTE_BENCHMARKS.md @@ -1,6 +1,6 @@ # Two-machine benchmarks -Linux and Zig 0.16.0 are required. `bench-server` is a dedicated echo/measurement +Linux and Zig 0.17.0 are required. `bench-server` is a dedicated echo/measurement server, not an example application server. Allow sufficient locked-memory budget for its io_uring workers (`ulimit -l`; local verification/CI use 8192 KiB). An insufficient budget can fail startup with `SystemResources`, even after buffer @@ -14,7 +14,7 @@ DTLS-PSK, and one active measurement session at a time. Configuration is CLI-onl On the server machine, bind explicitly to its WireGuard address: ```sh -zig build bench-server -Doptimize=ReleaseFast +zig build bench-server -Doptimize=fast ./zig-out/bin/bench-server --bind --port 5683 \ --threads 8 --allocator smp --exchange-count 4096 ``` @@ -28,13 +28,13 @@ On the client machine: ```sh # Completed CON responses over the client's measurement interval. -zig build bench -Doptimize=ReleaseFast -- \ +zig build bench -Doptimize=fast -- \ --no-server --host --port 5683 \ --threads 8 --multi-only --multi-window 256 \ --plain-only --con-only --allocator smp --count 3000000 # Unique NON requests received at the remote handler, including explicit loss. -zig build bench -Doptimize=ReleaseFast -- \ +zig build bench -Doptimize=fast -- \ --no-server --host --port 5683 \ --threads 8 --multi-only --plain-only --non-only \ --allocator smp --count 3000000 @@ -79,7 +79,7 @@ values or printed; owned parsing/key buffers are cleared after use. --threads 8 --allocator smp --exchange-count 4096 --psk-file bench-psk.json # Client machine; replace --con-only with --non-only for receipt accounting. -zig build bench -Doptimize=ReleaseFast -- \ +zig build bench -Doptimize=fast -- \ --no-server --host --port 5684 \ --threads 8 --multi-only --multi-window 256 --allocator smp \ --dtls-only --con-only --psk-file bench-psk.json --count 3000000 @@ -165,8 +165,8 @@ payloads. Concurrent requests needed to fill it depend on RTT, not just cores. ```sh zig build test -zig build test -Doptimize=ReleaseSafe -zig build -Doptimize=ReleaseFast +zig build test -Doptimize=safe +zig build -Doptimize=fast python3 bench/remote-smoke.py ``` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b16b09b..828f4dd 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -270,7 +270,7 @@ Any work on the above must preserve these properties: 3. **No per-packet syscalls beyond io_uring** — batch submission 4. **Common-case paths pay no tax for optional features** — separate responses, block transfers, observe registries must not add overhead when unused -5. **Benchmark regression gate** — run `zig build bench -Doptimize=ReleaseFast` +5. **Benchmark regression gate** — run `zig build bench -Doptimize=fast` before and after. No regressions in req/s or p99 latency for plain NON/CON echo workloads. diff --git a/docs/ZIG_017_PERFORMANCE.md b/docs/ZIG_017_PERFORMANCE.md new file mode 100644 index 0000000..a21b2a5 --- /dev/null +++ b/docs/ZIG_017_PERFORMANCE.md @@ -0,0 +1,119 @@ +# Zig 0.17 migration: performance checks + +These are historical measurements of the original migration (`00b0935`), +before rebasing onto the later I/O and benchmark changes in `871c8a2`. +They do not establish performance parity for the rebased branch. + +## Rebase verification + +On `871c8a2` plus this migration, 217 tests pass in debug, safe, fast and +safe/baseline-CPU configurations. Normal/optimized builds, documentation, +ARM64 cross-builds and remote benchmark smoke checks pass. Smoke checks cover +IPv4/IPv6, plain/DTLS, single/multi clients and the remote accounting protocol. +The full 24-scenario optimized suite reports zero request errors; an isolated +1000-byte single-thread DTLS NON check measures 417,149 req/s with zero errors. +These are verification runs, not a matched performance comparison with latest +main. Historical throughput/tail conclusions below must not be extrapolated to +the rebased branch. + +## Historical method + +Compare the merged Zig 0.16 tree (`39c42dc`, identical to PR #93's `11d3b52`) +with this Zig 0.17 migration. Both use optimized native-CPU builds: +`-Doptimize=ReleaseFast` on Zig 0.16.0 and `-Doptimize=fast` on Zig 0.17.0. +The machine is an AMD Ryzen AI MAX+ 395 running Linux 7.1.11. No governor or +system tuning was changed. + +Each of the 24 scenarios uses five sequential, alternating baseline/migration +runs: 500,000 requests, 10,000 warmup requests, CPU affinity 0–7. Each process +runs only one scenario. Multi-threaded cases use four clients/server workers; +windows are 256 (single-threaded) and 64 per client (multi-threaded). + +```sh +zig build -Doptimize=fast +taskset -c 0-7 ./zig-out/bin/bench --threads 4 --count 500000 \ + --warmup 10000 --dtls-only --non-only --single-only --payload-only 1000 +``` + +Use the other transport/type/threading/payload filters to reproduce the matrix. +Run the two versions sequentially, never concurrently. Separate invocations +avoid outstanding NON traffic contaminating the next scenario's counters. + +## Regression found and repaired + +A syntax-only migration compiled and passed the crypto vectors, but caused a +repeatable large-payload regression: single-threaded 1000-byte DTLS NON fell +from 330,652 to 254,277 req/s (-23.1%), and CON fell 12.1%. + +An encryption/decryption microbenchmark using runtime keys and non-inlined +entry points reproduced the problem without networking: 1000-byte encryption +rose from about 625 to 1064 ns/op. A constant-key benchmark largely hid it. +Disassembly showed repeated byte extractions/insertions in the loop-carried +CBC-MAC state. Zig 0.17's logical array/vector `@bitCast` lowering made the +previous fixed-width XOR unexpectedly expensive. + +The full-block XOR now uses explicit `*align(1)` byte-vector views rather than +logical bit casts. Three post-fix runtime-key microbenchmark runs measured +about 618 versus 626 ns/op for encryption and 621 versus 625 ns/op for +decryption. These measurements isolate CCM and are not network latency claims. + +Independent AESCCM vectors still cover full and partial block boundaries. +An additional test uses deliberately unaligned plaintext/ciphertext/output, +checks the independent 1000-byte ciphertext/tag digest, and verifies that +authentication failure zeros the output. Safe, fast and software-AES tests pass. + +## Final end-to-end medians + +Throughput is req/s; positive changes are improvements. p99 is microseconds. +All rows below were collected after the CCM repair. + +| Transport/type | Threads | Bytes | Zig 0.16 | Zig 0.17 | Change | p99 0.16 → 0.17 | +|---|---:|---:|---:|---:|---:|---:| +| Plain NON | 1 | 0 | 938,079 | 918,021 | -2.1% | — | +| Plain NON | 1 | 100 | 901,224 | 905,209 | +0.4% | — | +| Plain NON | 1 | 1000 | 775,558 | 786,585 | +1.4% | — | +| Plain NON | 4 | 0 | 1,954,575 | 2,152,937 | +10.1% | — | +| Plain NON | 4 | 100 | 1,784,826 | 2,007,386 | +12.5% | — | +| Plain NON | 4 | 1000 | 1,132,483 | 1,567,608 | +38.4% | — | +| Plain CON | 1 | 0 | 783,799 | 794,518 | +1.4% | 620.0 → 576.1 | +| Plain CON | 1 | 100 | 792,186 | 791,230 | -0.1% | 516.1 → 641.0 | +| Plain CON | 1 | 1000 | 742,504 | 741,343 | -0.2% | 664.7 → 648.3 | +| Plain CON | 4 | 0 | 1,546,872 | 1,142,689 | -26.1% | 213.1 → 262.1 | +| Plain CON | 4 | 100 | 1,048,441 | 1,490,036 | +42.1% | 269.4 → 248.4 | +| Plain CON | 4 | 1000 | 1,002,461 | 1,436,344 | +43.3% | 287.1 → 229.7 | +| DTLS NON | 1 | 0 | 619,367 | 620,424 | +0.2% | — | +| DTLS NON | 1 | 100 | 546,332 | 548,172 | +0.3% | — | +| DTLS NON | 1 | 1000 | 330,496 | 327,682 | -0.9% | — | +| DTLS NON | 4 | 0 | 1,513,865 | 1,501,675 | -0.8% | — | +| DTLS NON | 4 | 100 | 1,458,563 | 1,285,490 | -11.9% | — | +| DTLS NON | 4 | 1000 | 609,841 | 748,835 | +22.8% | — | +| DTLS CON | 1 | 0 | 177,544 | 176,657 | -0.5% | 2260.4 → 2318.9 | +| DTLS CON | 1 | 100 | 168,250 | 166,458 | -1.1% | 2463.5 → 2372.3 | +| DTLS CON | 1 | 1000 | 142,200 | 141,128 | -0.8% | 2594.8 → 2620.1 | +| DTLS CON | 4 | 0 | 153,617 | 156,667 | +2.0% | 2698.4 → 2620.0 | +| DTLS CON | 4 | 100 | 157,151 | 158,945 | +1.1% | 2603.7 → 2541.8 | +| DTLS CON | 4 | 1000 | 179,698 | 180,645 | +0.5% | 2263.1 → 2287.8 | + +## Interpretation and remaining limitations + +The reproducible large-payload DTLS regression is repaired; single-threaded +throughput medians are within about 2.1% of the Zig 0.16 baseline. This does +not establish regression-free performance or resolve historical Zig 0.15 +comparison concerns in [the earlier report](ZIG_016_PERFORMANCE.md). + +Plain CON 1T/100B p99 is higher in the final sample (516.1 → 641.0 µs), while +an earlier five-run set measured it lower (627.1 → 598.7 µs). That discrepancy +warrants caution about tail-latency conclusions rather than a claim of parity. + +Four-thread results remain highly sensitive to SO_REUSEPORT hashing of +ephemeral client source ports and scheduler placement. For example, Plain +CON 4T/0B migration throughput ranged from 1,004,074 to 1,514,403 req/s, +versus 1,008,000 to 2,782,266 for the baseline. The -26.1% median is a regression +candidate, not proof of an intrinsic slowdown; large positive medians are +similarly not reliable improvement claims. Controlled client-flow/worker +placement and additional profiling are needed to settle these cases. + +All sampled runs reported zero request errors. NON is fire-and-forget, so +zero errors does not establish zero UDP loss. Loopback results do not predict +real-network performance. ARM64 and big-endian PowerPC tests were +cross-compiled, not executed; no cross-target runtime results are claimed. diff --git a/src/Client.zig b/src/Client.zig index 0ddd6ff..1caaedd 100644 --- a/src/Client.zig +++ b/src/Client.zig @@ -512,9 +512,9 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !Client { .rng = rng, .dtls_session = if (effective_config.psk != null) std.mem.zeroInit(dtls.Session.Session, .{ .addr = platform.ip4(.{ 0, 0, 0, 0 }, 0) }) else null, .dtls_client_hs_state = .idle, - .dtls_cached_session_id = .{0} ** 32, + .dtls_cached_session_id = @splat(0), .dtls_cached_session_id_len = 0, - .dtls_cached_master_secret = .{0} ** 48, + .dtls_cached_master_secret = @splat(0), }; } @@ -925,7 +925,7 @@ fn processResponse(client: *Client, idx: u16, data: []const u8) !?Completion { }; client.peer_confirmed = true; if (packet.kind == .confirmable) client.sendAck(packet.msg_id, &.{}) catch |err| return client.finish(idx, err); - if (@intFromEnum(packet.code) >> 5 != 2) + if (@backingInt(packet.code) >> 5 != 2) return client.finish(idx, .{ .code = packet.code, .options = packet.options, .payload = packet.payload, .packet = packet }); if (packet.find_option(.block2)) |option| { const block = option.as_block() orelse return client.finish(idx, error.InvalidBlock2); @@ -969,7 +969,7 @@ fn continueBlock2(client: *Client, idx: u16, num: u32, szx: u3) !void { count += 1; const Order = struct { fn less(_: void, a: coapz.Option, b: coapz.Option) bool { - return @intFromEnum(a.kind) < @intFromEnum(b.kind); + return @backingInt(a.kind) < @backingInt(b.kind); } }; std.mem.sort(coapz.Option, options[0..count], {}, Order.less); @@ -1197,7 +1197,7 @@ pub fn upload( combined[opt_count] = block_opt; std.mem.sort(coapz.Option, combined[0 .. opt_count + 1], {}, struct { fn less(_: void, a: coapz.Option, b: coapz.Option) bool { - return @intFromEnum(a.kind) < @intFromEnum(b.kind); + return @backingInt(a.kind) < @backingInt(b.kind); } }.less); @@ -1787,7 +1787,7 @@ test "plain and DTLS Block1/Block2 responses are bounded and allocation-free" { const uploaded = try client.upload(upload_storage.buffers(), .put, &.{ .{ .kind = .uri_path, .value = "large" }, .{ .kind = .size1, .value = &.{ 8, 0 } }, - }, &([_]u8{7} ** 2048)); + }, &@as([2048]u8, @splat(7))); try testing.expectEqualSlices(u8, &bounded_test_body, uploaded.payload); // The second reassembly must not overwrite the first result or metadata. try testing.expectEqualSlices(u8, &bounded_test_body, result.packet.payload); @@ -1815,7 +1815,7 @@ test "cancel, timeout, and serialization failure release caller buffer borrows" try testing.expectEqual(again, completion.handle); try testing.expectError(error.Timeout, completion.result); try testing.expectEqual(@as(u16, 0), client.count_active); - try testing.expectError(error.BufferTooSmall, client.submit(storage.buffers(), .get, &.{}, &([_]u8{0} ** 2000))); + try testing.expectError(error.BufferTooSmall, client.submit(storage.buffers(), .get, &.{}, &@as([2000]u8, @splat(0)))); try testing.expectEqual(@as(u16, 0), client.count_active); const valid = try client.submit(storage.buffers(), .get, &.{}, "reused"); client.cancel(valid); @@ -1850,7 +1850,7 @@ test "Block2 handles reduced SZX, duplicate separate blocks, and retained option .{ .kind = .size1, .value = &.{55} }, }, &.{}); const token = client.slots[h].token; - try testing.expect((try injectTestBlock(&client, h, 0, 1, true, &([_]u8{1} ** 32), "tag", .acknowledgement)) == null); + try testing.expect((try injectTestBlock(&client, h, 0, 1, true, &@as([32]u8, @splat(1)), "tag", .acknowledgement)) == null); try testing.expectEqualSlices(u8, token[0..client.config.token_len], client.slots[h].token[0..client.config.token_len]); var decoded_options: [17]coapz.Option = undefined; const pending = &client.slots[h]; @@ -1860,13 +1860,13 @@ test "Block2 handles reduced SZX, duplicate separate blocks, and retained option try testing.expectEqual(.block2, continuation.options[2].kind); try testing.expectEqual(.size1, continuation.options[3].kind); // Retransmitted separate block: ACK it without duplicate assembly. - try testing.expect((try injectTestBlock(&client, h, 0, 1, true, &([_]u8{1} ** 32), "tag", .confirmable)) == null); + try testing.expect((try injectTestBlock(&client, h, 0, 1, true, &@as([32]u8, @splat(1)), "tag", .confirmable)) == null); try testing.expectEqual(@as(usize, 32), client.slots[h].block2_length); - try testing.expect((try injectTestBlock(&client, h, 2, 0, true, &([_]u8{2} ** 16), "tag", .acknowledgement)) == null); + try testing.expect((try injectTestBlock(&client, h, 2, 0, true, &@as([16]u8, @splat(2)), "tag", .acknowledgement)) == null); const completion = (try injectTestBlock(&client, h, 3, 0, false, "1234567", "tag", .acknowledgement)).?; try testing.expectEqual(h, completion.handle); const result = try completion.result; - try testing.expectEqualSlices(u8, &([_]u8{1} ** 32 ++ [_]u8{2} ** 16 ++ "1234567".*), result.payload); + try testing.expectEqualSlices(u8, &(@as([32]u8, @splat(1)) ++ @as([16]u8, @splat(2)) ++ "1234567".*), result.payload); const next = try client.submit(.{ .packet = &.{}, .options = &.{} }, .get, &.{}, &.{}); client.cancel(next); try testing.expectEqualSlices(u8, "tag", result.packet.find_option(.etag).?.value); @@ -1881,20 +1881,20 @@ test "Block2 ETag mismatch and invalid offsets complete with correlated errors" defer client.deinit(); var storage: ResponseStorage(64) = .{}; const h = try client.submit(storage.buffers(), .get, &.{}, &.{}); - try testing.expect((try injectTestBlock(&client, h, 0, 0, true, &([_]u8{1} ** 16), "a", .acknowledgement)) == null); + try testing.expect((try injectTestBlock(&client, h, 0, 0, true, &@as([16]u8, @splat(1)), "a", .acknowledgement)) == null); const mismatch = (try injectTestBlock(&client, h, 1, 0, false, "end", "b", .acknowledgement)).?; try testing.expectEqual(h, mismatch.handle); try testing.expectError(error.EtagMismatch, mismatch.result); try testing.expectEqual(@as(u16, 0), client.count_active); const h2 = try client.submit(storage.buffers(), .get, &.{}, &.{}); - const invalid = (try injectTestBlock(&client, h2, 1, 0, true, &([_]u8{1} ** 16), "a", .acknowledgement)).?; + const invalid = (try injectTestBlock(&client, h2, 1, 0, true, &@as([16]u8, @splat(1)), "a", .acknowledgement)).?; try testing.expectEqual(h2, invalid.handle); try testing.expectError(error.InvalidBlock2, invalid.result); try testing.expectEqual(@as(u16, 0), client.count_active); var block_buf: [3]u8 = undefined; const last = (coapz.BlockValue{ .num = 0xfffff, .szx = 0, .more = false }).option(.block2, &block_buf); const h3 = try client.submit(storage.buffers(), .get, &.{last}, &.{}); - const exhausted = (try injectTestBlock(&client, h3, 0xfffff, 0, true, &([_]u8{1} ** 16), "a", .acknowledgement)).?; + const exhausted = (try injectTestBlock(&client, h3, 0xfffff, 0, true, &@as([16]u8, @splat(1)), "a", .acknowledgement)).?; try testing.expectEqual(h3, exhausted.handle); try testing.expectError(error.InvalidBlock2, exhausted.result); try testing.expectEqual(@as(u16, 0), client.count_active); diff --git a/src/Io.zig b/src/Io.zig index abc3acf..152cccb 100644 --- a/src/Io.zig +++ b/src/Io.zig @@ -137,7 +137,7 @@ pub fn recv_multishot( const sqe = try io.ring.get_sqe(); sqe.prep_recvmsg_multishot(0, msg_header, MSG_TRUNC); sqe.flags |= linux.IOSQE_BUFFER_SELECT | linux.IOSQE_FIXED_FILE; - sqe.user_data = @intFromEnum(UserData.recv_msg); + sqe.user_data = @backingInt(UserData.recv_msg); sqe.buf_index = constants.buffer_group_id; } @@ -150,7 +150,7 @@ pub fn send_msg( const sqe = try io.ring.get_sqe(); sqe.prep_sendmsg(0, msg_header, 0); sqe.flags |= linux.IOSQE_FIXED_FILE; - sqe.user_data = @intFromEnum(UserData.send_msg); + sqe.user_data = @backingInt(UserData.send_msg); } /// Wait for at least `wait_count` completions. Does not submit. @@ -205,8 +205,8 @@ pub fn decode_recv(io: *const Io, cqe: *const Cqe) !RecvResult { return error.PayloadOutOfBounds; } - // Determine address family from the first 2 bytes of the name area. - const peer_family = std.mem.readInt(u16, io.buffers[name_offset..][0..2], .little); + // sockaddr's family is native-endian, unlike its network-endian port. + const peer_family = std.mem.bytesToValue(u16, io.buffers[name_offset..][0..2]); var peer: platform.SocketAddress = undefined; const address_len: usize = switch (peer_family) { @@ -230,17 +230,17 @@ pub fn decode_recv(io: *const Io, cqe: *const Cqe) !RecvResult { /// Ensures `wait_cqes` unblocks periodically so the server can /// check shutdown flags even when no packets arrive. pub fn queue_timeout(io: *Io, ts: *const linux.kernel_timespec) !void { - _ = try io.ring.timeout(@intFromEnum(UserData.tick_timeout), ts, 0, 0); + _ = try io.ring.timeout(@backingInt(UserData.tick_timeout), ts, 0, 0); } /// Check whether a CQE is a recv_msg completion. pub fn is_recv(cqe: *const Cqe) bool { - return cqe.user_data == @intFromEnum(UserData.recv_msg); + return cqe.user_data == @backingInt(UserData.recv_msg); } /// Check whether a CQE is a tick timeout completion. pub fn is_timeout(cqe: *const Cqe) bool { - return cqe.user_data == @intFromEnum(UserData.tick_timeout); + return cqe.user_data == @backingInt(UserData.tick_timeout); } /// Check whether a CQE completed successfully. @@ -261,6 +261,37 @@ test "provided ring init cleans up after allocation failures" { try testing.checkAllAllocationFailures(testing.allocator, checkInitAllocation, .{}); } +test "decode_recv preserves IPv4 and scoped IPv6 socket addresses" { + const addresses = [_]std.Io.net.IpAddress{ + platform.ip4(.{ 1, 2, 3, 4 }, 0x1234), + try platform.parseIp("fe80::1%3", 5684), + }; + for (addresses) |address| { + var buffer: [64]u8 align(@alignOf(linux.io_uring_recvmsg_out)) = @splat(0); + const sa = platform.SocketAddress.init(address); + const header: *linux.io_uring_recvmsg_out = @ptrCast(&buffer); + header.* = .{ .namelen = sa.len(), .controllen = 0, .payloadlen = 4, .flags = 0 }; + const name_offset = @sizeOf(linux.io_uring_recvmsg_out); + @memcpy(buffer[name_offset..][0..sa.len()], sa.bytes()); + @memcpy(buffer[name_offset + sa.len() ..][0..4], "ping"); + const io: Io = .{ + .ring = undefined, + .buffers = &buffer, + .iovecs = &.{}, + .buffer_count = 1, + .buffer_size = buffer.len, + .fd_socket = null, + .buffer_ring = undefined, + .buffer_ring_entries = 0, + }; + const cqe: Cqe = .{ .user_data = 0, .res = 0, .flags = linux.IORING_CQE_F_BUFFER }; + const result = try io.decode_recv(&cqe); + try testing.expect(platform.addressEqual(address, result.peer_address)); + try testing.expectEqualStrings("ping", result.payload); + try testing.expectEqual(@as(u16, 0), result.buffer_id); + } +} + test "SO_REUSEPORT allows two instances on same port" { const port: u16 = 19691; const allocator = testing.allocator; diff --git a/src/Server.zig b/src/Server.zig index 9de3354..94a008a 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -364,8 +364,8 @@ fn init_raw( errdefer if (rate_limiter) |*rl| rl.deinit(allocator); var dtls_sessions: ?*dtls.Session.SessionTable = null; - var dtls_cookie_secret: [32]u8 = .{0} ** 32; - var dtls_cookie_secret_prev: [32]u8 = .{0} ** 32; + var dtls_cookie_secret: [32]u8 = @splat(0); + var dtls_cookie_secret_prev: [32]u8 = @splat(0); if (config.psk != null) { const tbl_ptr = try allocator.create(dtls.Session.SessionTable); errdefer allocator.destroy(tbl_ptr); @@ -1413,18 +1413,18 @@ fn process_dtls_record( server.process_dtls_coap(record.payload, sess, peer, index); }, .alert => { - if (record.payload.len >= 2 and record.payload[0] == @intFromEnum(dtls.types.AlertLevel.fatal)) { + if (record.payload.len >= 2 and record.payload[0] == @backingInt(dtls.types.AlertLevel.fatal)) { log.debug("DTLS: received fatal alert {d}", .{record.payload[1]}); tbl.release(sess); return; - } else if (record.payload.len >= 2 and record.payload[1] == @intFromEnum(dtls.types.AlertDescription.close_notify)) { + } else if (record.payload.len >= 2 and record.payload[1] == @backingInt(dtls.types.AlertDescription.close_notify)) { log.debug("DTLS: close_notify from peer", .{}); tbl.release(sess); return; } }, else => { - log.debug("DTLS: unexpected content type in encrypted record: {d}", .{@intFromEnum(record.content_type)}); + log.debug("DTLS: unexpected content type in encrypted record: {d}", .{@backingInt(record.content_type)}); }, } } else { @@ -1875,7 +1875,7 @@ fn handleBlock1( ) ?Block1Result { // Extract Request-Tag (RFC 9175 §3) for Block1 disambiguation. // Request-Tag option number 292 (RFC 9175). - const request_tag_kind: coapz.OptionKind = @enumFromInt(292); + const request_tag_kind: coapz.OptionKind = @fromBackingInt(@intCast(292)); const request_tag: []const u8 = if (packet.find_option(request_tag_kind)) |rt| rt.value else &.{}; if (bv.num == 0) { @@ -2467,7 +2467,7 @@ fn hasUnrecognizedCriticalOption( options: []const coapz.Option, ) bool { for (options) |opt| { - const num = @intFromEnum(opt.kind); + const num = @backingInt(opt.kind); if (num & 1 == 0) continue; // elective if (isRecognizedOption(opt.kind)) continue; if (std.mem.indexOfScalar(u16, server.config.recognized_options, num) != null) continue; @@ -3180,7 +3180,7 @@ test "CON with unknown critical option returns 4.02 Bad Option" { .msg_id = 0xCC01, .token = &.{0x01}, .options = &.{ - .{ .kind = @enumFromInt(99), .value = "x" }, + .{ .kind = @fromBackingInt(@intCast(99)), .value = "x" }, }, .payload = "hello", .data_buf = &.{}, @@ -3219,7 +3219,7 @@ test "NON with unknown critical option returns 4.02 Bad Option" { .msg_id = 0xCC02, .token = &.{0x02}, .options = &.{ - .{ .kind = @enumFromInt(99), .value = "x" }, + .{ .kind = @fromBackingInt(@intCast(99)), .value = "x" }, }, .payload = "hello", .data_buf = &.{}, @@ -3294,7 +3294,7 @@ test "unknown elective option passes through to handler" { .msg_id = 0xCC04, .token = &.{0x04}, .options = &.{ - .{ .kind = @enumFromInt(100), .value = "ignored" }, + .{ .kind = @fromBackingInt(@intCast(100)), .value = "ignored" }, }, .payload = "data", .data_buf = &.{}, @@ -3333,7 +3333,7 @@ test "recognized_options allows custom critical options" { .msg_id = 0xCC05, .token = &.{0x05}, .options = &.{ - .{ .kind = @enumFromInt(99), .value = "custom" }, + .{ .kind = @fromBackingInt(@intCast(99)), .value = "custom" }, }, .payload = "ok", .data_buf = &.{}, diff --git a/src/block_transfer.zig b/src/block_transfer.zig index d4d5b34..2e59cd4 100644 --- a/src/block_transfer.zig +++ b/src/block_transfer.zig @@ -65,9 +65,9 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !BlockTransfer { slot.* = .{ .state = .free, .kind = .block1_receiving, - .token = .{0} ** 8, + .token = @splat(0), .token_len = 0, - .request_tag = .{0} ** 8, + .request_tag = @splat(0), .request_tag_len = 0, .peer_address = platform.ip4(.{ 0, 0, 0, 0 }, 0), .payload_length = 0, @@ -116,14 +116,14 @@ pub fn allocate( .state = .active, .kind = kind, .token = blk: { - var t: [8]u8 = .{0} ** 8; + var t: [8]u8 = @splat(0); const len = @min(token.len, 8); @memcpy(t[0..len], token[0..len]); break :blk t; }, .token_len = @intCast(@min(token.len, 8)), .request_tag = blk: { - var rt: [8]u8 = .{0} ** 8; + var rt: [8]u8 = @splat(0); const len = @min(request_tag.len, 8); @memcpy(rt[0..len], request_tag[0..len]); break :blk rt; @@ -304,7 +304,7 @@ test "block1: reassemble three fragments" { const idx = pool.allocate(&.{0xAA}, addr, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; - const block = [_]u8{0x42} ** 1024; + const block: [1024]u8 = @splat(0x42); try testing.expect(pool.appendBlock1(idx, 0, true, &block) == .more); try testing.expect(pool.appendBlock1(idx, 1, true, &block) == .more); try testing.expect(pool.appendBlock1(idx, 2, false, &block) == .complete); @@ -335,7 +335,7 @@ test "block1: error on payload too large" { const idx = pool.allocate(&.{0xAA}, addr, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; - const big = [_]u8{0x42} ** 101; + const big: [101]u8 = @splat(0x42); try testing.expect(pool.appendBlock1(idx, 0, false, &big) == .error_too_large); } @@ -344,7 +344,7 @@ test "block2: store payload and serve blocks" { defer pool.deinit(testing.allocator); const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); - const payload = [_]u8{0x42} ** 2500; + const payload: [2500]u8 = @splat(0x42); const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -371,7 +371,7 @@ test "block2: smaller SZX negotiation" { defer pool.deinit(testing.allocator); const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); - const payload = [_]u8{0x42} ** 600; + const payload: [600]u8 = @splat(0x42); const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -417,7 +417,7 @@ test "block2: payload_length readable after release for Size2" { const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; - const payload = [_]u8{0x42} ** 500; + const payload: [500]u8 = @splat(0x42); pool.storeBlock2Payload(idx, &payload); // Serve last block (only block for 500 bytes at SZX=6=1024). diff --git a/src/constants.zig b/src/constants.zig index f968926..3de218c 100644 --- a/src/constants.zig +++ b/src/constants.zig @@ -1,6 +1,5 @@ /// Default configuration values and CoAP transmission parameters. /// RFC 7252 section 4.8. - /// Default number of provided buffers for io_uring. /// Must exceed the max burst between tick() calls (client window size) /// plus headroom for packets arriving during CQE processing. diff --git a/src/deferred.zig b/src/deferred.zig index 75536da..268c2ca 100644 --- a/src/deferred.zig +++ b/src/deferred.zig @@ -121,7 +121,7 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !Deferred { for (slots, 0..) |*slot, i| { slot.* = .{ .state = std.atomic.Value(State).init(.free), - .token = .{0} ** 8, + .token = @splat(0), .token_len = 0, .peer_address = platform.ip4(.{ 0, 0, 0, 0 }, 0), .msg_id = 0, @@ -173,7 +173,7 @@ pub fn allocate( slot.* = .{ .state = std.atomic.Value(State).init(.pending), .token = blk: { - var t: [8]u8 = .{0} ** 8; + var t: [8]u8 = @splat(0); const len = @min(token.len, 8); @memcpy(t[0..len], token[0..len]); break :blk t; diff --git a/src/dtls/Ccm.zig b/src/dtls/Ccm.zig index f6387fd..b291253 100644 --- a/src/dtls/Ccm.zig +++ b/src/dtls/Ccm.zig @@ -129,7 +129,7 @@ fn ctrEncrypt(aes: AesEncryptCtx(Aes128), nonce: [nonce_len]u8, src: []const u8, // Final partial block. if (offset < src.len) { - var pad: [block_len]u8 = .{0} ** block_len; + var pad: [block_len]u8 = @splat(0); const tail = src.len - offset; @memcpy(pad[0..tail], src[offset..][0..tail]); aes.xor(&pad, &pad, makeCtrBlock(nonce, counter)); @@ -166,7 +166,7 @@ fn computeCbcMac(aes: AesEncryptCtx(Aes128), plaintext: []const u8, ad: []const var tmp: [block_len]u8 = undefined; // Build the first AD block: [len_hi, len_lo, ad[0..14 or less]] - var ad_block: [block_len]u8 = [_]u8{0} ** block_len; + var ad_block: [block_len]u8 = @splat(0); ad_block[0] = @intCast((ad.len >> 8) & 0xFF); ad_block[1] = @intCast(ad.len & 0xFF); const first_chunk = @min(block_len - 2, ad.len); @@ -187,13 +187,14 @@ fn computeCbcMac(aes: AesEncryptCtx(Aes128), plaintext: []const u8, ad: []const } } - // Keep the full-block XOR fixed-width. A variable-sized byte loop here - // prevents efficient vectorization with Zig 0.16 for large DTLS records. + // Keep the XOR fixed-width and reinterpret byte storage directly. + // Zig 0.17's logical array/vector bitCast can otherwise scalarize the + // loop-carried MAC into costly byte extractions and insertions. var pt_offset: usize = 0; while (plaintext.len - pt_offset >= block_len) : (pt_offset += block_len) { - const block = plaintext[pt_offset..][0..block_len].*; - x = @bitCast(@as(@Vector(block_len, u8), @bitCast(x)) ^ - @as(@Vector(block_len, u8), @bitCast(block))); + const mac: *align(1) @Vector(block_len, u8) = @ptrCast(&x); + const block: *align(1) const @Vector(block_len, u8) = @ptrCast(plaintext[pt_offset..][0..block_len]); + mac.* ^= block.*; var tmp: [block_len]u8 = undefined; aes.encrypt(&tmp, &x); x = tmp; @@ -227,28 +228,55 @@ test "CCM block and wide-block boundaries match independent AESCCM vectors" { .{ 1000, "380b19c3ee26288dd91955fec5cb5ff370baa9b95fbf8d97c77f000dbb7da2b0" }, .{ 1024, "7d181bca0e31a073d7e6cf80572e6df2042a4679e4fd402c7d6ad7ca16b3f9b6" }, }; + const ad: [13]u8 = @splat(4); + const nonce: [nonce_len]u8 = @splat(3); + const key: [key_len]u8 = @splat(7); var plaintext: [1024]u8 = undefined; for (&plaintext, 0..) |*b, i| b.* = @intCast(i % 251); var ciphertext: [1032]u8 = undefined; var recovered: [1024]u8 = undefined; inline for (vectors) |v| { const len = v[0]; - encrypt(plaintext[0..len], &(.{4} ** 13), .{3} ** 12, .{7} ** 16, ciphertext[0 .. len + tag_len]); + encrypt(plaintext[0..len], &ad, nonce, key, ciphertext[0 .. len + tag_len]); var digest: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(ciphertext[0 .. len + tag_len], &digest, .{}); var expected: [32]u8 = undefined; _ = try std.fmt.hexToBytes(&expected, v[1]); try std.testing.expectEqualSlices(u8, &expected, &digest); - try decrypt(ciphertext[0 .. len + tag_len], &(.{4} ** 13), .{3} ** 12, .{7} ** 16, recovered[0..len]); + try decrypt(ciphertext[0 .. len + tag_len], &ad, nonce, key, recovered[0..len]); try std.testing.expectEqualSlices(u8, plaintext[0..len], recovered[0..len]); } } const testing = std.testing; +test "CCM full-block vector views support unaligned buffers" { + var storage: [1001]u8 align(16) = undefined; + var encrypted: [1009]u8 align(16) = undefined; + var recovered: [1001]u8 align(16) = undefined; + const plaintext = storage[1..]; + const ciphertext = encrypted[1..]; + const output = recovered[1..]; + for (plaintext, 0..) |*b, i| b.* = @intCast(i % 251); + const key: [key_len]u8 = @splat(7); + const nonce: [nonce_len]u8 = @splat(3); + const ad: [13]u8 = @splat(4); + encrypt(plaintext, &ad, nonce, key, ciphertext); + var digest: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(ciphertext, &digest, .{}); + var expected: [32]u8 = undefined; + _ = try std.fmt.hexToBytes(&expected, "380b19c3ee26288dd91955fec5cb5ff370baa9b95fbf8d97c77f000dbb7da2b0"); + try testing.expectEqualSlices(u8, &expected, &digest); + try decrypt(ciphertext, &ad, nonce, key, output); + try testing.expectEqualSlices(u8, plaintext, output); + ciphertext[ciphertext.len - 1] ^= 1; + try testing.expectError(error.AuthenticationFailed, decrypt(ciphertext, &ad, nonce, key, output)); + try testing.expectEqualSlices(u8, &@as([1000]u8, @splat(0)), output); +} + test "round-trip: basic encrypt/decrypt" { - const key = [_]u8{0x01} ** key_len; - const nonce = [_]u8{0x02} ** nonce_len; + const key: [key_len]u8 = @splat(0x01); + const nonce: [nonce_len]u8 = @splat(0x02); const plaintext = "Hello, DTLS!"; const ad = "additional data"; @@ -261,8 +289,8 @@ test "round-trip: basic encrypt/decrypt" { } test "authentication failure on tampered ciphertext" { - const key = [_]u8{0xAB} ** key_len; - const nonce = [_]u8{0xCD} ** nonce_len; + const key: [key_len]u8 = @splat(0xAB); + const nonce: [nonce_len]u8 = @splat(0xCD); const plaintext = "secret message"; const ad = "header"; @@ -278,8 +306,8 @@ test "authentication failure on tampered ciphertext" { } test "authentication failure on tampered tag" { - const key = [_]u8{0x11} ** key_len; - const nonce = [_]u8{0x22} ** nonce_len; + const key: [key_len]u8 = @splat(0x11); + const nonce: [nonce_len]u8 = @splat(0x22); const plaintext = "data"; const ad = "ad"; @@ -295,8 +323,8 @@ test "authentication failure on tampered tag" { } test "empty plaintext (tag only)" { - const key = [_]u8{0x42} ** key_len; - const nonce = [_]u8{0x99} ** nonce_len; + const key: [key_len]u8 = @splat(0x42); + const nonce: [nonce_len]u8 = @splat(0x99); const plaintext = ""; const ad = "some header"; @@ -310,8 +338,8 @@ test "empty plaintext (tag only)" { } test "empty plaintext empty ad" { - const key = [_]u8{0x55} ** key_len; - const nonce = [_]u8{0x66} ** nonce_len; + const key: [key_len]u8 = @splat(0x55); + const nonce: [nonce_len]u8 = @splat(0x66); var ciphertext: [tag_len]u8 = undefined; encrypt("", "", nonce, key, &ciphertext); @@ -321,8 +349,8 @@ test "empty plaintext empty ad" { } test "1-byte plaintext" { - const key = [_]u8{0xAA} ** key_len; - const nonce = [_]u8{0xBB} ** nonce_len; + const key: [key_len]u8 = @splat(0xAA); + const nonce: [nonce_len]u8 = @splat(0xBB); const plaintext = [_]u8{0xFF}; const ad = "hdr"; @@ -335,9 +363,9 @@ test "1-byte plaintext" { } test "16-byte plaintext (exact block)" { - const key = [_]u8{0x12} ** key_len; - const nonce = [_]u8{0x34} ** nonce_len; - const plaintext = [_]u8{0x56} ** block_len; + const key: [key_len]u8 = @splat(0x12); + const nonce: [nonce_len]u8 = @splat(0x34); + const plaintext: [block_len]u8 = @splat(0x56); const ad = "block-aligned"; var ciphertext: [plaintext.len + tag_len]u8 = undefined; @@ -349,9 +377,9 @@ test "16-byte plaintext (exact block)" { } test "17-byte plaintext (crosses block boundary)" { - const key = [_]u8{0x78} ** key_len; - const nonce = [_]u8{0x9A} ** nonce_len; - const plaintext = [_]u8{0xBC} ** 17; + const key: [key_len]u8 = @splat(0x78); + const nonce: [nonce_len]u8 = @splat(0x9A); + const plaintext: [17]u8 = @splat(0xBC); const ad = "cross-block"; var ciphertext: [plaintext.len + tag_len]u8 = undefined; @@ -363,9 +391,9 @@ test "17-byte plaintext (crosses block boundary)" { } test "32-byte plaintext (two full blocks)" { - const key = [_]u8{0xDE} ** key_len; - const nonce = [_]u8{0xAD} ** nonce_len; - const plaintext = [_]u8{0xBE} ** 32; + const key: [key_len]u8 = @splat(0xDE); + const nonce: [nonce_len]u8 = @splat(0xAD); + const plaintext: [32]u8 = @splat(0xBE); const ad = "two blocks"; var ciphertext: [plaintext.len + tag_len]u8 = undefined; @@ -377,8 +405,8 @@ test "32-byte plaintext (two full blocks)" { } test "wrong AD causes authentication failure" { - const key = [_]u8{0x33} ** key_len; - const nonce = [_]u8{0x44} ** nonce_len; + const key: [key_len]u8 = @splat(0x33); + const nonce: [nonce_len]u8 = @splat(0x44); const plaintext = "payload"; const ad = "correct-ad"; @@ -391,8 +419,8 @@ test "wrong AD causes authentication failure" { } test "output zeroed on auth failure" { - const key = [_]u8{0x77} ** key_len; - const nonce = [_]u8{0x88} ** nonce_len; + const key: [key_len]u8 = @splat(0x77); + const nonce: [nonce_len]u8 = @splat(0x88); const plaintext = "sensitive"; const ad = "hdr"; @@ -402,16 +430,16 @@ test "output zeroed on auth failure" { // Tamper ciphertext[0] ^= 0x01; - var recovered: [plaintext.len]u8 = [_]u8{0xFF} ** plaintext.len; + var recovered: [plaintext.len]u8 = @splat(0xFF); _ = decrypt(&ciphertext, ad, nonce, key, &recovered) catch {}; // After auth failure, output should be zeroed - try testing.expectEqualSlices(u8, &([_]u8{0} ** plaintext.len), &recovered); + try testing.expectEqualSlices(u8, &@as([plaintext.len]u8, @splat(0)), &recovered); } test "different nonces produce different ciphertexts" { - const key = [_]u8{0xEE} ** key_len; - const nonce1 = [_]u8{0x01} ** nonce_len; - const nonce2 = [_]u8{0x02} ** nonce_len; + const key: [key_len]u8 = @splat(0xEE); + const nonce1: [nonce_len]u8 = @splat(0x01); + const nonce2: [nonce_len]u8 = @splat(0x02); const plaintext = "same plaintext"; const ad = "same ad"; diff --git a/src/dtls/Cookie.zig b/src/dtls/Cookie.zig index ee34e20..fd76ed9 100644 --- a/src/dtls/Cookie.zig +++ b/src/dtls/Cookie.zig @@ -31,38 +31,38 @@ pub fn verifyWithRotation(current_secret: [32]u8, previous_secret: [32]u8, clien } test "generate and verify round-trip" { - const secret = [_]u8{0x42} ** 32; + const secret: [32]u8 = @splat(0x42); const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); - const random = [_]u8{0xab} ** 32; + const random: [32]u8 = @splat(0xab); const cookie = generate(secret, addr, random); try std.testing.expect(verify(secret, addr, random, &cookie)); } test "verify fails with wrong address" { - const secret = [_]u8{0x01} ** 32; + const secret: [32]u8 = @splat(0x01); const addr1 = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); const addr2 = platform.ip4([4]u8{ 192, 168, 1, 1 }, 5684); - const random = [_]u8{0x22} ** 32; + const random: [32]u8 = @splat(0x22); const cookie = generate(secret, addr1, random); try std.testing.expect(!verify(secret, addr2, random, &cookie)); } test "verify fails with wrong secret" { - const secret1 = [_]u8{0x01} ** 32; - const secret2 = [_]u8{0x02} ** 32; + const secret1: [32]u8 = @splat(0x01); + const secret2: [32]u8 = @splat(0x02); const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); - const random = [_]u8{0x33} ** 32; + const random: [32]u8 = @splat(0x33); const cookie = generate(secret1, addr, random); try std.testing.expect(!verify(secret2, addr, random, &cookie)); } test "verify fails with wrong length (truncated cookie)" { - const secret = [_]u8{0x55} ** 32; + const secret: [32]u8 = @splat(0x55); const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); - const random = [_]u8{0x66} ** 32; + const random: [32]u8 = @splat(0x66); const cookie = generate(secret, addr, random); try std.testing.expect(!verify(secret, addr, random, cookie[0..16])); @@ -71,20 +71,20 @@ test "verify fails with wrong length (truncated cookie)" { } test "verify fails with wrong client_random" { - const secret = [_]u8{0x77} ** 32; + const secret: [32]u8 = @splat(0x77); const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); - const random1 = [_]u8{0x88} ** 32; - const random2 = [_]u8{0x99} ** 32; + const random1: [32]u8 = @splat(0x88); + const random2: [32]u8 = @splat(0x99); const cookie = generate(secret, addr, random1); try std.testing.expect(!verify(secret, addr, random2, &cookie)); } test "verifyWithRotation accepts cookie from previous secret" { - const current = [_]u8{0xaa} ** 32; - const previous = [_]u8{0xbb} ** 32; + const current: [32]u8 = @splat(0xaa); + const previous: [32]u8 = @splat(0xbb); const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); - const random = [_]u8{0xcc} ** 32; + const random: [32]u8 = @splat(0xcc); const cookie = generate(previous, addr, random); try std.testing.expect(verifyWithRotation(current, previous, addr, random, &cookie)); @@ -94,11 +94,11 @@ test "verifyWithRotation accepts cookie from previous secret" { } test "verifyWithRotation rejects cookie from neither secret" { - const current = [_]u8{0xdd} ** 32; - const previous = [_]u8{0xee} ** 32; - const other = [_]u8{0xff} ** 32; + const current: [32]u8 = @splat(0xdd); + const previous: [32]u8 = @splat(0xee); + const other: [32]u8 = @splat(0xff); const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); - const random = [_]u8{0x11} ** 32; + const random: [32]u8 = @splat(0x11); const cookie = generate(other, addr, random); try std.testing.expect(!verifyWithRotation(current, previous, addr, random, &cookie)); diff --git a/src/dtls/Handshake.zig b/src/dtls/Handshake.zig index 83e8092..0921ed3 100644 --- a/src/dtls/Handshake.zig +++ b/src/dtls/Handshake.zig @@ -80,7 +80,7 @@ pub fn encodeHandshakeMessage(msg_type: types.HandshakeType, message_seq: u16, b const total = handshake_header_len + body.len; std.debug.assert(out.len >= total); const body_len: u24 = @intCast(body.len); - out[0] = @intFromEnum(msg_type); + out[0] = @backingInt(msg_type); out[1] = @intCast((body_len >> 16) & 0xFF); out[2] = @intCast((body_len >> 8) & 0xFF); out[3] = @intCast(body_len & 0xFF); @@ -103,7 +103,7 @@ pub fn decodeHandshakeMessage(buf: []const u8) ?HandshakeMessage { if (frag_offset != 0 or frag_len != body_len) return null; if (buf.len < handshake_header_len + body_len) return null; return .{ - .msg_type = @enumFromInt(buf[0]), + .msg_type = @fromBackingInt(@intCast(buf[0])), .message_seq = std.mem.readInt(u16, buf[4..6], .big), .body = buf[handshake_header_len..][0..body_len], }; @@ -362,7 +362,7 @@ fn serverHandleClientHello( sh_body[34] = session.session_id_len; @memcpy(sh_body[35..][0..session.session_id_len], session.session_id[0..session.session_id_len]); const cs_off = 35 + @as(usize, session.session_id_len); - std.mem.writeInt(u16, sh_body[cs_off..][0..2], @intFromEnum(types.CipherSuite.tls_psk_with_aes_128_ccm_8), .big); + std.mem.writeInt(u16, sh_body[cs_off..][0..2], @backingInt(types.CipherSuite.tls_psk_with_aes_128_ccm_8), .big); sh_body[cs_off + 2] = 0; // compression = null const sh_len = cs_off + 3; @@ -952,7 +952,7 @@ fn buildClientHelloBody(client_random: [32]u8, cookie: []const u8, session_id: [ // cipher_suites: length(2) + TLS_PSK_WITH_AES_128_CCM_8(2) std.mem.writeInt(u16, out[off..][0..2], 2, .big); off += 2; - std.mem.writeInt(u16, out[off..][0..2], @intFromEnum(types.CipherSuite.tls_psk_with_aes_128_ccm_8), .big); + std.mem.writeInt(u16, out[off..][0..2], @backingInt(types.CipherSuite.tls_psk_with_aes_128_ccm_8), .big); off += 2; // compression_methods: length(1) + null(1) @@ -980,7 +980,7 @@ test "buildPreMasterSecret: known PSK" { // First 2 bytes: PSK length (9) in big-endian. try testing.expectEqual(@as(u16, 9), std.mem.readInt(u16, pms[0..2], .big)); // Next 9 bytes: zeroes. - try testing.expectEqualSlices(u8, &([_]u8{0} ** 9), pms[2..11]); + try testing.expectEqualSlices(u8, &@as([9]u8, @splat(0)), pms[2..11]); // Next 2 bytes: PSK length again. try testing.expectEqual(@as(u16, 9), std.mem.readInt(u16, pms[11..13], .big)); // Final 9 bytes: PSK value. @@ -992,8 +992,8 @@ test "deriveKeys: deterministic and distinct" { var pms_buf: [256]u8 = undefined; const pms = buildPreMasterSecret(psk, &pms_buf); - const client_random = [_]u8{0x11} ** 32; - const server_random = [_]u8{0x22} ** 32; + const client_random: [32]u8 = @splat(0x11); + const server_random: [32]u8 = @splat(0x22); const master = deriveMasterSecret(pms, client_random, server_random); const keys1 = deriveKeys(master, server_random, client_random); @@ -1025,7 +1025,7 @@ test "handshake message encode/decode round-trip" { } test "decodeHandshakeMessage: too short" { - const short = [_]u8{0} ** 11; + const short: [11]u8 = @splat(0); try testing.expect(decodeHandshakeMessage(&short) == null); } @@ -1033,7 +1033,7 @@ test "full server handshake" { const psk_identity = "test-identity"; const psk_key = "test-key-value"; const psk = types.Psk{ .identity = psk_identity, .key = psk_key }; - const cookie_secret = [_]u8{0x42} ** 32; + const cookie_secret: [32]u8 = @splat(0x42); const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); // Create a server session. @@ -1140,7 +1140,7 @@ test "full client-server handshake integration" { const psk_identity = "my-device"; const psk_key = "my-secret-key"; const psk = types.Psk{ .identity = psk_identity, .key = psk_key }; - const cookie_secret = [_]u8{0xAA} ** 32; + const cookie_secret: [32]u8 = @splat(0xAA); const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); var server_session: Session.Session = undefined; @@ -1321,10 +1321,10 @@ fn initTestSession(session: *Session.Session, addr: std.Io.net.IpAddress) void { .state = .handshaking, .peer_hash = 0, .addr = addr, - .client_write_key = .{0} ** 16, - .server_write_key = .{0} ** 16, - .client_write_iv = .{0} ** 4, - .server_write_iv = .{0} ** 4, + .client_write_key = @splat(0), + .server_write_key = @splat(0), + .client_write_iv = @splat(0), + .server_write_iv = @splat(0), .read_epoch = 0, .write_epoch = 0, .read_sequence = 0, @@ -1332,16 +1332,16 @@ fn initTestSession(session: *Session.Session, addr: std.Io.net.IpAddress) void { .replay_window = 0, .handshake_state = .idle, .handshake_hash = Sha256.init(.{}), - .client_random = .{0} ** 32, - .server_random = .{0} ** 32, + .client_random = @splat(0), + .server_random = @splat(0), .message_seq = 0, .retransmit_deadline_ns = 0, .retransmit_count = 0, .retransmit_timeout_ms = 0, - .last_flight = .{0} ** 256, + .last_flight = @splat(0), .last_flight_len = 0, - .master_secret = .{0} ** 48, - .session_id = .{0} ** 32, + .master_secret = @splat(0), + .session_id = @splat(0), .session_id_len = 0, .last_activity_ns = 0, .lru_prev = 0xFFFFFFFF, @@ -1352,7 +1352,7 @@ fn initTestSession(session: *Session.Session, addr: std.Io.net.IpAddress) void { test "session resumption: server abbreviated path with ResumptionData" { const psk = types.Psk{ .identity = "test-device", .key = "my-secret-key" }; - const cookie_secret = [_]u8{0xAA} ** 32; + const cookie_secret: [32]u8 = @splat(0xAA); const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); // Simulate a previously established session with known master_secret. @@ -1449,7 +1449,7 @@ test "session resumption: server abbreviated path with ResumptionData" { try testing.expectEqualSlices(u8, &saved_session_id, &server_session.session_id); try testing.expectEqualSlices(u8, &saved_master_secret, &server_session.master_secret); try testing.expect(server_session.write_epoch == 1); // CCS was sent - try testing.expect(!std.mem.eql(u8, &server_session.server_write_key, &([_]u8{0} ** 16))); // keys derived + try testing.expect(!std.mem.eql(u8, &server_session.server_write_key, &@as([16]u8, @splat(0)))); // keys derived } test "session resumption: client sends cached session_id" { diff --git a/src/dtls/Prf.zig b/src/dtls/Prf.zig index 3248722..c1ebbd4 100644 --- a/src/dtls/Prf.zig +++ b/src/dtls/Prf.zig @@ -97,7 +97,7 @@ test "prf: known-answer" { // seed = 64 zero bytes (client_random || server_random placeholder) const secret = "psk identity"; const label = "master secret"; - const seed = [_]u8{0x00} ** 64; + const seed: [64]u8 = @splat(0x00); // Expected: first 48 bytes of PRF output (standard master_secret length) const expected = [48]u8{ diff --git a/src/dtls/Record.zig b/src/dtls/Record.zig index 7890267..172071b 100644 --- a/src/dtls/Record.zig +++ b/src/dtls/Record.zig @@ -26,7 +26,7 @@ fn parseHeader(buf: []const u8) ?Header { // handshake for backward compatibility (e.g. HelloVerifyRequest). if (buf[1] != 0xFE or (buf[2] != 0xFD and buf[2] != 0xFF)) return null; return .{ - .content_type = @as(types.ContentType, @enumFromInt(buf[0])), + .content_type = @as(types.ContentType, @fromBackingInt(@intCast(buf[0]))), .epoch = std.mem.readInt(u16, buf[3..5], .big), .sequence_number = std.mem.readInt(u48, buf[5..11], .big), .length = std.mem.readInt(u16, buf[11..13], .big), @@ -42,7 +42,7 @@ fn writeHeader( length: u16, ) void { std.debug.assert(out.len >= types.record_header_len); - out[0] = @intFromEnum(content_type); + out[0] = @backingInt(content_type); // Version: DTLS 1.2 = 0xFE 0xFD out[1] = 0xFE; out[2] = 0xFD; @@ -58,7 +58,7 @@ fn buildAd(epoch: u16, seq: u48, content_type: types.ContentType, plaintext_len: var ad: [types.record_header_len]u8 = undefined; std.mem.writeInt(u16, ad[0..2], epoch, .big); std.mem.writeInt(u48, ad[2..8], seq, .big); - ad[8] = @intFromEnum(content_type); + ad[8] = @backingInt(content_type); ad[9] = 0xFE; // DTLS 1.2 ad[10] = 0xFD; std.mem.writeInt(u16, ad[11..13], plaintext_len, .big); @@ -267,8 +267,8 @@ test "encode/decode plaintext record (epoch 0) round-trip" { test "encode/decode encrypted record (epoch 1) round-trip" { const payload = "secret data"; - const key = [_]u8{0x01} ** 16; - const iv = [_]u8{0x02} ** 4; + const key: [16]u8 = @splat(0x01); + const iv: [4]u8 = @splat(0x02); var seq: u48 = 5; var buf: [types.record_overhead + payload.len]u8 = undefined; @@ -291,8 +291,8 @@ test "encode/decode encrypted record (epoch 1) round-trip" { test "anti-replay window rejects duplicate" { const payload = "data"; - const key = [_]u8{0xAA} ** 16; - const iv = [_]u8{0xBB} ** 4; + const key: [16]u8 = @splat(0xAA); + const iv: [4]u8 = @splat(0xBB); var seq: u48 = 10; var buf: [types.record_overhead + payload.len]u8 = undefined; @@ -312,8 +312,8 @@ test "anti-replay window rejects duplicate" { } test "anti-replay window accepts out-of-order within window" { - const key = [_]u8{0x11} ** 16; - const iv = [_]u8{0x22} ** 4; + const key: [16]u8 = @splat(0x11); + const iv: [4]u8 = @splat(0x22); const payload = "pkt"; // Encode records with seq 0, 1, 2. diff --git a/src/dtls/Session.zig b/src/dtls/Session.zig index 6753f41..fa09120 100644 --- a/src/dtls/Session.zig +++ b/src/dtls/Session.zig @@ -203,10 +203,10 @@ pub const SessionTable = struct { .state = .handshaking, .peer_hash = hash, .addr = addr, - .client_write_key = .{0} ** 16, - .server_write_key = .{0} ** 16, - .client_write_iv = .{0} ** 4, - .server_write_iv = .{0} ** 4, + .client_write_key = @splat(0), + .server_write_key = @splat(0), + .client_write_iv = @splat(0), + .server_write_iv = @splat(0), .read_epoch = 0, .write_epoch = 0, .read_sequence = 0, @@ -214,16 +214,16 @@ pub const SessionTable = struct { .replay_window = 0, .handshake_state = .idle, .handshake_hash = Sha256.init(.{}), - .client_random = .{0} ** 32, - .server_random = .{0} ** 32, + .client_random = @splat(0), + .server_random = @splat(0), .message_seq = 0, .retransmit_deadline_ns = 0, .retransmit_count = 0, .retransmit_timeout_ms = 0, - .last_flight = .{0} ** 256, + .last_flight = @splat(0), .last_flight_len = 0, - .master_secret = .{0} ** 48, - .session_id = .{0} ** 32, + .master_secret = @splat(0), + .session_id = @splat(0), .session_id_len = 0, .last_activity_ns = now_ns, .lru_prev = empty_sentinel, @@ -437,9 +437,9 @@ test "release zeros key material" { try testing.expectEqual(@as(u32, 0), tbl.count_active); // Keys should be zeroed by zeroKeys(). - try testing.expectEqualSlices(u8, &([_]u8{0} ** 16), &session.client_write_key); - try testing.expectEqualSlices(u8, &([_]u8{0} ** 16), &session.server_write_key); - try testing.expectEqualSlices(u8, &([_]u8{0} ** 48), &session.master_secret); + try testing.expectEqualSlices(u8, &@as([16]u8, @splat(0)), &session.client_write_key); + try testing.expectEqualSlices(u8, &@as([16]u8, @splat(0)), &session.server_write_key); + try testing.expectEqualSlices(u8, &@as([48]u8, @splat(0)), &session.master_secret); // Lookup should find nothing. try testing.expect(tbl.lookup(addr) == null); diff --git a/src/dtls/integration_test.zig b/src/dtls/integration_test.zig index 57623da..080f8b4 100644 --- a/src/dtls/integration_test.zig +++ b/src/dtls/integration_test.zig @@ -263,7 +263,7 @@ test "Block1: handler receives full reassembled body" { defer client.deinit(); // 2048-byte payload — requires multiple blocks at default szx=6 (1024 bytes). - const payload = [_]u8{0x42} ** 2048; + const payload: [2048]u8 = @splat(0x42); var path_buf: [1]coapz.Option = .{coapz.Option{ .kind = .uri_path, .value = "data" }}; block1_received_len = 0; var storage: Client.ResponseStorage(0) = .{}; @@ -540,7 +540,7 @@ test "DTLS: Block1 upload delivers reassembled body" { try client.handshake(); try testing.expectEqual(.established, client.dtls_session.?.state); - const payload = [_]u8{0x42} ** 2048; + const payload: [2048]u8 = @splat(0x42); var path_buf: [1]coapz.Option = .{coapz.Option{ .kind = .uri_path, .value = "data" }}; block1_received_len = 0; var storage: Client.ResponseStorage(0) = .{}; diff --git a/src/dtls/types.zig b/src/dtls/types.zig index 6a516c8..457db70 100644 --- a/src/dtls/types.zig +++ b/src/dtls/types.zig @@ -86,8 +86,8 @@ pub fn isDtlsContentType(byte: u8) bool { /// Encode a DTLS alert record (2 bytes: level + description). pub fn encodeAlert(level: AlertLevel, desc: AlertDescription, out: *[2]u8) void { - out[0] = @intFromEnum(level); - out[1] = @intFromEnum(desc); + out[0] = @backingInt(level); + out[1] = @backingInt(desc); } test "isDtlsContentType" { diff --git a/src/handler.zig b/src/handler.zig index 2d911fa..f7982b3 100644 --- a/src/handler.zig +++ b/src/handler.zig @@ -45,7 +45,7 @@ pub const Request = struct { /// Observe context. Non-null when the server has an observer registry. observe_ctx: ?ObserveContext = null, /// Route parameters captured by the router (e.g. `:id` segments). - route_params: [max_route_params]RouteParam = [_]RouteParam{.{}} ** max_route_params, + route_params: [max_route_params]RouteParam = @splat(.{}), route_param_count: u8 = 0, payload_override: ?[]const u8 = null, @@ -128,7 +128,7 @@ pub const Request = struct { /// Return the Echo option value from this request, if present (RFC 9175 §2). /// The handler can compare this with a previously sent Echo to verify freshness. pub fn echoOption(self: Request) ?[]const u8 { - const echo_kind: coapz.OptionKind = @enumFromInt(252); + const echo_kind: coapz.OptionKind = @fromBackingInt(@intCast(252)); if (self.packet.find_option(echo_kind)) |opt| return opt.value; return null; } @@ -292,7 +292,7 @@ pub const Response = struct { /// verification. The client must reflect the Echo value in subsequent /// requests. Generates a random 8-byte Echo value. pub fn withEcho(self: Response, arena: std.mem.Allocator) Response { - const echo_kind: coapz.OptionKind = @enumFromInt(252); + const echo_kind: coapz.OptionKind = @fromBackingInt(@intCast(252)); var echo_val: [8]u8 = undefined; platform.randomBytes(&echo_val); const echo_opt = coapz.Option{ .kind = echo_kind, .value = arena.dupe(u8, &echo_val) catch return self }; diff --git a/src/observe.zig b/src/observe.zig index c23d8a6..c2ad85a 100644 --- a/src/observe.zig +++ b/src/observe.zig @@ -80,7 +80,7 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !ObserverRegistry { for (observers) |*obs| { obs.* = .{ .peer_address = platform.ip4(.{ 0, 0, 0, 0 }, 0), - .token = .{0} ** 8, + .token = @splat(0), .token_len = 0, }; } @@ -162,7 +162,7 @@ pub fn addObserver(self: *ObserverRegistry, resource_id: u16, peer: std.Io.net.I obs.active = true; obs.peer_address = peer; obs.token_len = @intCast(@min(token.len, 8)); - obs.token = .{0} ** 8; + obs.token = @splat(0); @memcpy(obs.token[0..obs.token_len], token[0..obs.token_len]); obs.notify_count = 0; obs.pending_con_msg_id = 0; @@ -252,7 +252,7 @@ pub fn notify(self: *ObserverRegistry, resource_id: u16, response: handler.Respo // Layout: [code:1][obs_seq:4 LE][opts_count:1][opts...][payload...] // Each option: [kind:2 LE][len:2 LE][value:len] const resp_opts: u8 = @intCast(@min(response.options.len, 15)); - buf[0] = @intFromEnum(response.code); + buf[0] = @backingInt(response.code); std.mem.writeInt(u32, buf[1..5], seq, .little); buf[5] = resp_opts; var buf_pos: usize = 6; @@ -262,7 +262,7 @@ pub fn notify(self: *ObserverRegistry, resource_id: u16, response: handler.Respo qs.seq.store(pos +% 1, .release); return; } - std.mem.writeInt(u16, buf[buf_pos..][0..2], @intFromEnum(opt.kind), .little); + std.mem.writeInt(u16, buf[buf_pos..][0..2], @backingInt(opt.kind), .little); std.mem.writeInt(u16, buf[buf_pos + 2 ..][0..2], @intCast(opt.value.len), .little); buf_pos += 4; @memcpy(buf[buf_pos..][0..opt.value.len], opt.value); @@ -332,7 +332,7 @@ pub const NotifyMeta = struct { /// `arena` is used to allocate the options array. pub fn decodeNotifyMeta(data: []const u8, arena: std.mem.Allocator) ?NotifyMeta { if (data.len < 6) return null; - const code: coapz.Code = @enumFromInt(data[0]); + const code: coapz.Code = @fromBackingInt(@intCast(data[0])); const obs_seq = std.mem.readInt(u32, data[1..5], .little); const opts_count = data[5]; var buf_pos: usize = 6; @@ -340,7 +340,7 @@ pub fn decodeNotifyMeta(data: []const u8, arena: std.mem.Allocator) ?NotifyMeta const options = arena.alloc(coapz.Option, @as(usize, opts_count) + 1) catch return null; for (0..opts_count) |i| { if (buf_pos + 4 > data.len) return null; - const kind: coapz.OptionKind = @enumFromInt(std.mem.readInt(u16, data[buf_pos..][0..2], .little)); + const kind: coapz.OptionKind = @fromBackingInt(@intCast(std.mem.readInt(u16, data[buf_pos..][0..2], .little))); const vlen = std.mem.readInt(u16, data[buf_pos + 2 ..][0..2], .little); buf_pos += 4; if (buf_pos + vlen > data.len) return null; diff --git a/src/packet_view.zig b/src/packet_view.zig index 929028f..6f7a2cd 100644 --- a/src/packet_view.zig +++ b/src/packet_view.zig @@ -48,17 +48,17 @@ pub fn read(data: []const u8, options: []coapz.Option) Error!coapz.Packet { kind = std.math.add(u16, kind, delta) catch return error.InvalidOption; if (len > data.len - pos) return error.TruncatedOption; if (count >= options.len) return error.ResponseOptionsTooSmall; - options[count] = .{ .kind = @enumFromInt(kind), .value = data[pos..][0..len] }; + options[count] = .{ .kind = @fromBackingInt(@intCast(kind)), .value = data[pos..][0..len] }; count += 1; pos += len; } - return .{ .kind = @enumFromInt((data[0] >> 4) & 3), .code = @enumFromInt(data[1]), .msg_id = std.mem.readInt(u16, data[2..4], .big), .token = data[4..][0..tkl], .options = options[0..count], .payload = payload, .data_buf = &.{} }; + return .{ .kind = @fromBackingInt(@intCast((data[0] >> 4) & 3)), .code = @fromBackingInt(@intCast(data[1])), .msg_id = std.mem.readInt(u16, data[2..4], .big), .token = data[4..][0..tkl], .options = options[0..count], .payload = payload, .data_buf = &.{} }; } test "packet view decodes extended lengths, deltas, and repeated options" { - const a = [_]u8{7} ** 13; - const b = [_]u8{9} ** 269; - const packet: coapz.Packet = .{ .kind = .confirmable, .code = .content, .msg_id = 500, .token = "12345678", .payload = "end", .data_buf = &.{}, .options = &.{ .{ .kind = .uri_path, .value = &a }, .{ .kind = .uri_path, .value = &.{} }, .{ .kind = .size1, .value = &b }, .{ .kind = @enumFromInt(64000), .value = "x" } } }; + const a: [13]u8 = @splat(7); + const b: [269]u8 = @splat(9); + const packet: coapz.Packet = .{ .kind = .confirmable, .code = .content, .msg_id = 500, .token = "12345678", .payload = "end", .data_buf = &.{}, .options = &.{ .{ .kind = .uri_path, .value = &a }, .{ .kind = .uri_path, .value = &.{} }, .{ .kind = .size1, .value = &b }, .{ .kind = @fromBackingInt(@intCast(64000)), .value = "x" } } }; var wire: [1024]u8 = undefined; const data = try packet.writeBuf(&wire); var options: [4]coapz.Option = undefined; diff --git a/src/platform.zig b/src/platform.zig index 3bbb1e7..9250ee7 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -25,7 +25,7 @@ pub const SocketAddress = extern union { pub fn init(a: std.Io.net.IpAddress) SocketAddress { return switch (a) { - .ip4 => |v| .{ .ip4 = .{ .port = std.mem.nativeToBig(u16, v.port), .addr = @bitCast(v.bytes) } }, + .ip4 => |v| .{ .ip4 = .{ .port = std.mem.nativeToBig(u16, v.port), .addr = std.mem.bytesToValue(u32, &v.bytes) } }, .ip6 => |v| .{ .ip6 = .{ .port = std.mem.nativeToBig(u16, v.port), .addr = v.bytes, .flowinfo = std.mem.nativeToBig(u32, v.flow), .scope_id = v.interface.index } }, }; } @@ -40,7 +40,7 @@ pub const SocketAddress = extern union { pub fn address(a: *const SocketAddress) !std.Io.net.IpAddress { return switch (a.any.family) { - AF.INET => .{ .ip4 = .{ .port = std.mem.bigToNative(u16, a.ip4.port), .bytes = @bitCast(a.ip4.addr) } }, + AF.INET => .{ .ip4 = .{ .port = std.mem.bigToNative(u16, a.ip4.port), .bytes = std.mem.toBytes(a.ip4.addr) } }, AF.INET6 => .{ .ip6 = .{ .port = std.mem.bigToNative(u16, a.ip6.port), .bytes = a.ip6.addr, .flow = std.mem.bigToNative(u32, a.ip6.flowinfo), .interface = .{ .index = a.ip6.scope_id } } }, else => error.UnsupportedAddressFamily, }; @@ -189,7 +189,7 @@ pub fn dup2(old: fd_t, new: fd_t) SocketError!void { _ = try result(linux.dup2(old, new)); } pub fn waitpid(pid: pid_t, flags: u32) void { - var status: u32 = undefined; + var status: i32 = undefined; while (linux.errno(linux.waitpid(pid, &status, flags)) == .INTR) {} } @@ -213,6 +213,15 @@ test "UDP syscall boundary reports WouldBlock and transfers datagrams" { try std.testing.expectEqualStrings("ping", buffer[0..received]); } +test "IPv4 socket bytes preserve network byte order" { + const address = ip4(.{ 1, 2, 3, 4 }, 0x1234); + const sa = SocketAddress.init(address); + try std.testing.expectEqualSlices(u8, &.{ 0x12, 0x34, 1, 2, 3, 4 }, sa.bytes()[2..8]); + try std.testing.expectEqualSlices(u8, &@as([8]u8, @splat(0)), sa.bytes()[8..16]); + const decoded = try sa.address(); + try std.testing.expect(addressEqual(address, decoded)); +} + test "native addresses round-trip through Linux socket representation" { for ([_][]const u8{ "127.0.0.1", "::1", "fe80::1" }) |text| { const a = try std.Io.net.IpAddress.parse(text, 5683); diff --git a/src/rate_limiter.zig b/src/rate_limiter.zig index 4b43060..f82dcf1 100644 --- a/src/rate_limiter.zig +++ b/src/rate_limiter.zig @@ -23,14 +23,14 @@ pub const AddrKey = struct { family: u16, addr: [16]u8, - pub const zero: AddrKey = .{ .family = 0, .addr = .{0} ** 16 }; + pub const zero: AddrKey = .{ .family = 0, .addr = @splat(0) }; pub fn fromAddress(address: std.Io.net.IpAddress) AddrKey { return switch (platform.family(address)) { posix.AF.INET => .{ .family = posix.AF.INET, .addr = blk: { - var a: [16]u8 = .{0} ** 16; + var a: [16]u8 = @splat(0); const src = address.ip4.bytes; @memcpy(a[0..4], &src); break :blk a; @@ -50,7 +50,7 @@ pub const AddrKey = struct { pub fn hash(self: AddrKey) u64 { var h: u64 = 0xcbf29ce484222325; - const fam_bytes: [2]u8 = @bitCast(self.family); + const fam_bytes = std.mem.toBytes(self.family); for (fam_bytes) |b| { h ^= b; h *%= 0x100000001b3; @@ -320,8 +320,8 @@ fn ipv4Key(comptime ip: u32) AddrKey { return .{ .family = posix.AF.INET, .addr = blk: { - var a: [16]u8 = .{0} ** 16; - a[0..4].* = @bitCast(ip); + var a: [16]u8 = @splat(0); + a[0..4].* = std.mem.toBytes(ip); break :blk a; }, }; diff --git a/src/router.zig b/src/router.zig index d27c4d2..8738e3f 100644 --- a/src/router.zig +++ b/src/router.zig @@ -74,7 +74,7 @@ pub fn Router(comptime routes: anytype) type { const segments = comptime splitPath(path); var it = req.pathSegments(); var result = CaptureResult{ - .params = [_]Request.RouteParam{.{}} ** Request.max_route_params, + .params = @splat(.{}), .count = 0, }; inline for (segments) |expected| {