tiny.http.chunk
Defined in tiny.http.
API (13)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/http/src/chunk.zig
zig
const std = @import("std");pub const Error = error{ IncompleteBody, MalformedBody, BodyTooLarge, OutOfMemory,};pub const Scan = union(enum) { incomplete, malformed, complete: usize,};pub const Decode = struct { body: []u8, consumed: usize,};pub const Capacity = struct { decoded_bytes: usize, consumed: usize,};pub const Decoder = struct { state: State = .size, size: usize = 0, size_started: bool = false, size_finished: bool = false, extension: bool = false, remaining: usize = 0, trailer_empty: bool = true, done: bool = false, const State = enum { size, size_lf, data, data_cr, data_lf, trailer, trailer_lf, done, }; pub fn init() Decoder { return .{}; } pub fn feed(self: *Decoder, data: []const u8, handler: anytype) anyerror!usize { if (self.done) return 0; var index: usize = 0; while (index < data.len and !self.done) { switch (self.state) { .size => { const byte = data[index]; index += 1; if (byte == '\r') { if (!self.size_started) return error.MalformedBody; self.state = .size_lf; continue; } if (byte == '\n') return error.MalformedBody; if (self.extension) continue; if (byte == ';') { if (!self.size_started) return error.MalformedBody; self.extension = true; continue; } if (byte == ' ' or byte == '\t') { if (self.size_started) self.size_finished = true; continue; } const digit = hexDigit(byte) orelse return error.MalformedBody; if (self.size_finished) return error.MalformedBody; self.size = std.math.mul(usize, self.size, 16) catch { return error.MalformedBody; }; self.size = std.math.add(usize, self.size, digit) catch { return error.MalformedBody; }; self.size_started = true; }, .size_lf => { if (data[index] != '\n') return error.MalformedBody; index += 1; if (self.size == 0) { self.trailer_empty = true; self.state = .trailer; } else { self.remaining = self.size; self.state = .data; } }, .data => { const take = @min(self.remaining, data.len - index); if (take != 0) try handler.emit(data[index..][0..take]); index += take; self.remaining -= take; if (self.remaining == 0) self.state = .data_cr; }, .data_cr => { if (data[index] != '\r') return error.MalformedBody; index += 1; self.state = .data_lf; }, .data_lf => { if (data[index] != '\n') return error.MalformedBody; index += 1; self.resetSize(); }, .trailer => { const byte = data[index]; index += 1; if (byte == '\r') { self.state = .trailer_lf; } else if (byte == '\n') { return error.MalformedBody; } else { self.trailer_empty = false; } }, .trailer_lf => { if (data[index] != '\n') return error.MalformedBody; index += 1; if (self.trailer_empty) { self.done = true; self.state = .done; } else { self.trailer_empty = true; self.state = .trailer; } }, .done => unreachable, } } return index; } fn resetSize(self: *Decoder) void { self.state = .size; self.size = 0; self.size_started = false; self.size_finished = false; self.extension = false; }};fn hexDigit(byte: u8) ?usize { return switch (byte) { '0'...'9' => byte - '0', 'a'...'f' => byte - 'a' + 10, 'A'...'F' => byte - 'A' + 10, else => null, };}pub fn parseSize(line: []const u8) ?usize { if (line.len == 0) return null; const clean = if (std.mem.indexOf(u8, line, ";")) |sc| std.mem.trim(u8, line[0..sc], " \t") else line; if (clean.len == 0) return null; return std.fmt.parseInt(usize, clean, 16) catch null;}pub fn scanBody(data: []const u8) Scan { const capacity = survey(data, null) catch |err| switch (err) { error.IncompleteBody => return .incomplete, error.MalformedBody, error.BodyTooLarge => return .malformed, error.OutOfMemory => unreachable, }; return .{ .complete = capacity.consumed };}pub fn survey(data: []const u8, max_decoded_size: ?usize) Error!Capacity { var pos: usize = 0; var decoded_bytes: usize = 0; while (true) { const size_end = std.mem.indexOf(u8, data[pos..], "\r\n") orelse { return error.IncompleteBody; }; const size_line = std.mem.trim(u8, data[pos .. pos + size_end], " \t"); const chunk_size = parseSize(size_line) orelse return error.MalformedBody; pos += size_end + 2; if (chunk_size == 0) { while (true) { const trailer_end = std.mem.indexOf(u8, data[pos..], "\r\n") orelse { return error.IncompleteBody; }; pos += trailer_end + 2; if (trailer_end == 0) { return .{ .decoded_bytes = decoded_bytes, .consumed = pos }; } } } decoded_bytes = std.math.add(usize, decoded_bytes, chunk_size) catch { return error.BodyTooLarge; }; if (max_decoded_size) |limit| { if (decoded_bytes > limit) return error.BodyTooLarge; } if (chunk_size > data.len - pos) return error.IncompleteBody; const chunk_end = pos + chunk_size; if (data.len - chunk_end < 2) return error.IncompleteBody; if (data[chunk_end] != '\r' or data[chunk_end + 1] != '\n') { return error.MalformedBody; } pos = chunk_end + 2; }}pub fn decodeInto( output: []u8, data: []const u8, max_decoded_size: ?usize,) Error!Decode { const capacity = try survey(data, max_decoded_size); return decodeSurveyedInto(output, data, capacity);}pub fn decodeSurveyedInto(output: []u8, data: []const u8, capacity: Capacity) Error!Decode { if (capacity.decoded_bytes > output.len) return error.BodyTooLarge; var pos: usize = 0; var output_length: usize = 0; while (true) { const size_end = std.mem.indexOf(u8, data[pos..], "\r\n") orelse { return error.IncompleteBody; }; const chunk_size = parseSize(std.mem.trim( u8, data[pos .. pos + size_end], " \t", )) orelse return error.MalformedBody; pos += size_end + 2; if (chunk_size == 0) { if (output_length != capacity.decoded_bytes) return error.MalformedBody; return .{ .body = output[0..output_length], .consumed = capacity.consumed, }; } if (chunk_size > capacity.decoded_bytes - output_length) { return error.MalformedBody; } if (chunk_size > data.len - pos) return error.IncompleteBody; const chunk_end = pos + chunk_size; std.mem.copyForwards( u8, output[output_length..][0..chunk_size], data[pos..chunk_end], ); output_length += chunk_size; pos = chunk_end + 2; }}pub fn decodeAlloc( allocator: std.mem.Allocator, data: []const u8, max_decoded_size: ?usize,) Error!Decode { const capacity = try survey(data, max_decoded_size); const body = allocator.alloc(u8, capacity.decoded_bytes) catch { return error.OutOfMemory; }; errdefer allocator.free(body); return decodeSurveyedInto(body, data, capacity);}const testing = std.testing;const TestCollector = struct { allocator: std.mem.Allocator, buf: std.ArrayListUnmanaged(u8) = .empty, fn deinit(self: *TestCollector) void { self.buf.deinit(self.allocator); } fn emit(self: *TestCollector, bytes: []const u8) !void { try self.buf.appendSlice(self.allocator, bytes); }};const RejectingChunkHandler = struct { fn emit(_: *@This(), _: []const u8) !void { return error.ChunkRejected; }};const FixedChunkCollector = struct { body: [32]u8 = undefined, length: usize = 0, fn emit(self: *FixedChunkCollector, bytes: []const u8) !void { if (bytes.len > self.body.len - self.length) return error.TestBodyCapacityExceeded; @memcpy(self.body[self.length..][0..bytes.len], bytes); self.length += bytes.len; }};test "chunk scan distinguishes embedded terminal bytes from structure" { try testing.expectEqual(Scan.incomplete, scanBody("9\r\nabc0\r\n\r\n")); try testing.expectEqual(@as(usize, 19), scanBody("9\r\nabc0\r\n\r\nx\r\n0\r\n\r\n").complete);}test "chunk scan waits for trailer terminator" { try testing.expectEqual(Scan.incomplete, scanBody("0\r\nTrailer: value\r\n")); const data = "0\r\nTrailer: value\r\n\r\n"; try testing.expectEqual(data.len, scanBody(data).complete);}test "chunk decode single chunk" { const decoded = try decodeAlloc(testing.allocator, "5\r\nHello\r\n0\r\n\r\n", null); defer testing.allocator.free(decoded.body); try testing.expectEqualStrings("Hello", decoded.body);}test "chunk decode multiple chunks" { const decoded = try decodeAlloc(testing.allocator, "5\r\nHello\r\n1\r\n \r\n5\r\nWorld\r\n0\r\n\r\n", null); defer testing.allocator.free(decoded.body); try testing.expectEqualStrings("Hello World", decoded.body);}test "chunk decode accepts trailers" { const data = "5\r\nHello\r\n0\r\nDigest: sha-256=abc123\r\n\r\n"; const decoded = try decodeAlloc(testing.allocator, data, null); defer testing.allocator.free(decoded.body); try testing.expectEqualStrings("Hello", decoded.body); try testing.expectEqual(data.len, decoded.consumed);}test "chunk decode empty returns empty owned slice" { const decoded = try decodeAlloc(testing.allocator, "0\r\n\r\n", null); defer testing.allocator.free(decoded.body); try testing.expectEqual(@as(usize, 0), decoded.body.len);}test "chunk decode returns consumed length before extra bytes" { const decoded = try decodeAlloc(testing.allocator, "5\r\nHello\r\n0\r\n\r\nGET /next HTTP/1.1\r\n\r\n", null); defer testing.allocator.free(decoded.body); try testing.expectEqualStrings("Hello", decoded.body); try testing.expectEqual(@as(usize, 15), decoded.consumed);}test "chunk decode rejects incomplete final terminator" { try testing.expectError(error.IncompleteBody, decodeAlloc(testing.allocator, "0\r\n", null));}test "chunk decode preserves allocation failure" { var failing = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 }); try testing.expectError(error.OutOfMemory, decodeAlloc(failing.allocator(), "5\r\nHello\r\n0\r\n\r\n", null));}test "chunk decode enforces decoded size limit" { try testing.expectError(error.BodyTooLarge, decodeAlloc(testing.allocator, "5\r\nHello\r\n0\r\n\r\n", 4));}test "chunk survey reports decoded and consumed capacity" { const data = "3\r\none\r\n2\r\n!!\r\n0\r\nTrailer: value\r\n\r\nnext"; try testing.expectEqual(Capacity{ .decoded_bytes = 5, .consumed = data.len - "next".len, }, try survey(data, 5));}test "chunk decodeInto rejects short output before mutation" { var output = @as([4]u8, @splat(0xA5)); try testing.expectError( error.BodyTooLarge, decodeInto(&output, "5\r\nHello\r\n0\r\n\r\n", null), ); try testing.expectEqualSlices(u8, &.{ 0xA5, 0xA5, 0xA5, 0xA5 }, &output);}test "chunk decodeInto accepts exact output" { var output: [5]u8 = undefined; const decoded = try decodeInto(&output, "2\r\nHe\r\n3\r\nllo\r\n0\r\n\r\n", 5); try testing.expectEqualStrings("Hello", decoded.body);}test "chunk decodeInto supports in-place compaction" { var storage = [_]u8{ '2', '\r', '\n', 'H', 'e', '\r', '\n', '3', '\r', '\n', 'l', 'l', 'o', '\r', '\n', '0', '\r', '\n', '\r', '\n' }; const decoded = try decodeInto(&storage, &storage, storage.len); try testing.expectEqualStrings("Hello", decoded.body);}test "chunk streaming decoder emits data across boundaries" { var collector = TestCollector{ .allocator = testing.allocator }; defer collector.deinit(); var decoder = Decoder.init(); _ = try decoder.feed("4\r\nWiki\r\n", &collector); _ = try decoder.feed("5\r\npedia\r\n0\r\n\r\n", &collector); try testing.expectEqualStrings("Wikipedia", collector.buf.items);}test "chunk streaming decoder reports every exact terminal boundary" { const cases = [_]struct { wire: []const u8, encoded_length: usize, }{ .{ .wire = "3\r\nabc\r\n0\r\n\r\nTAIL", .encoded_length = "3\r\nabc\r\n0\r\n\r\n".len, }, .{ .wire = "3\r\nabc\r\n0\r\nTrace: value\r\n\r\nTAIL", .encoded_length = "3\r\nabc\r\n0\r\nTrace: value\r\n\r\n".len, }, }; for (cases) |case| { for (0..case.encoded_length + 1) |split| { var collector = FixedChunkCollector{}; var decoder = Decoder.init(); const before = try decoder.feed(case.wire[0..split], &collector); const after = try decoder.feed(case.wire[split..], &collector); const consumed = before + after; try testing.expect(decoder.done); try testing.expectEqual(case.encoded_length, consumed); try testing.expectEqualStrings("abc", collector.body[0..collector.length]); try testing.expectEqualStrings("TAIL", case.wire[consumed..]); } }}test "chunk streaming decoder accepts every byte boundary without storage" { const input = "4;kind=test\r\nWiki\r\n5\r\npedia\r\n0\r\nTrace: value\r\n\r\n"; var collector = FixedChunkCollector{}; var decoder = Decoder.init(); for (input, 0..) |_, index| { _ = try decoder.feed(input[index..][0..1], &collector); } try testing.expect(decoder.done); try testing.expectEqualStrings("Wikipedia", collector.body[0..collector.length]);}test "chunk streaming decoder accepts split chunk terminator" { var collector = TestCollector{ .allocator = testing.allocator }; defer collector.deinit(); var decoder = Decoder.init(); _ = try decoder.feed("4\r\nWiki", &collector); try testing.expect(!decoder.done); try testing.expectEqualStrings("Wiki", collector.buf.items); _ = try decoder.feed("\r", &collector); try testing.expect(!decoder.done); _ = try decoder.feed("\n0\r\n\r\n", &collector); try testing.expect(decoder.done); try testing.expectEqualStrings("Wiki", collector.buf.items);}test "chunk streaming decoder waits for trailer terminator" { var collector = TestCollector{ .allocator = testing.allocator }; defer collector.deinit(); var decoder = Decoder.init(); _ = try decoder.feed("0\r\n", &collector); try testing.expect(!decoder.done); _ = try decoder.feed("X-Trace: bench\r\n", &collector); try testing.expect(!decoder.done); _ = try decoder.feed("\r\n", &collector); try testing.expect(decoder.done); try testing.expectEqual(@as(usize, 0), collector.buf.items.len);}test "chunk streaming decoder preserves callback error identity" { var decoder = Decoder.init(); var failing = RejectingChunkHandler{}; try testing.expectError(error.ChunkRejected, decoder.feed("5\r\nHello\r\n0\r\n\r\n", &failing));}Source: lib/http/src/root.zig:27
zig
pub const chunk = @import("chunk.zig");Complete caller list for chunk.decodeAlloc
8 direct callers.
lib.http.src.chunk.test_chunk_decode_accepts_trailers[function] — test source atlib/http/src/chunk.zig:334in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_empty_returns_empty_owned_slice[function] — test source atlib/http/src/chunk.zig:343in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_enforces_decoded_size_limit[function] — test source atlib/http/src/chunk.zig:367in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_multiple_chunks[function] — test source atlib/http/src/chunk.zig:327in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_preserves_allocation_failure[function] — test source atlib/http/src/chunk.zig:362in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_rejects_incomplete_final_terminator[function] — test source atlib/http/src/chunk.zig:358in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_returns_consumed_length_before_extra_bytes[function] — test source atlib/http/src/chunk.zig:350in nearest public ownertiny.http.chunklib.http.src.chunk.test_chunk_decode_single_chunk[function] — test source atlib/http/src/chunk.zig:320in nearest public ownertiny.http.chunk
Audit
| Definitions | 14 |
|---|---|
| Public names | 14 |
| Members | 19 |
| Version | 26.7.0 |
| Revision | daab053ee433 |