Skip to documentation
SLOP

tiny.http.chunk

Reference tiny.http chunk

Defined in tiny.http.

API (13)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.httpchunk
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallstest sourcelib.http.src.chunktest: chunk streaming decoder accepts...test sourcelib.http.src.chunktest: chunk streaming decoder accepts...test sourcelib.http.src.chunktest: chunk streaming decoder emits d...test sourcelib.http.src.chunktest: chunk streaming decoder preserv...test sourcelib.http.src.chunktest: chunk streaming decoder reports...test sourcelib.http.src.chunktest: chunk streaming decoder waits f...private sourcelib.http.src.chunk.DecoderresetSizeprivate sourcelib.http.src.chunkhexDigitchunk.Decoderfeed
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstest sourcelib.http.src.chunktest: chunk streaming decoder accepts...test sourcelib.http.src.chunktest: chunk streaming decoder accepts...test sourcelib.http.src.chunktest: chunk streaming decoder emits d...test sourcelib.http.src.chunktest: chunk streaming decoder preserv...test sourcelib.http.src.chunktest: chunk streaming decoder reports...test sourcelib.http.src.chunktest: chunk streaming decoder waits f...chunk.Decoderinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.http.src.chunktest: chunk decode accepts trailerstest sourcelib.http.src.chunktest: chunk decode empty returns empt...test sourcelib.http.src.chunktest: chunk decode enforces decoded s...test sourcelib.http.src.chunktest: chunk decode multiple chunkstest sourcelib.http.src.chunktest: chunk decode preserves allocati...+3 morechunkdecodeSurveyedIntochunksurveychunkdecodeAlloc
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallstest sourcelib.http.src.chunktest: chunk decodeInto accepts exact ...test sourcelib.http.src.chunktest: chunk decodeInto rejects short ...test sourcelib.http.src.chunktest: chunk decodeInto supports in-pl...chunkdecodeSurveyedIntochunksurveychunkdecodeInto
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallschunkdecodeAllocchunkdecodeIntochunkparseSizechunkdecodeSurveyedInto
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callschunkdecodeSurveyedIntochunksurveychunkparseSize
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.http.src.chunktest: chunk scan distinguishes embedd...test sourcelib.http.src.chunktest: chunk scan waits for trailer te...chunksurveychunkscanBody
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallschunkdecodeAllocchunkdecodeIntochunkscanBodytest sourcelib.http.src.chunktest: chunk survey reports decoded an...chunkparseSizechunksurvey
Static calls · unresolved targets: 0 · external targets: 0.

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.

Audit

Definitions14
Public names14
Members19
Version26.7.0
Revisiondaab053ee433