master
  1//! Parser for transfer-encoding: chunked.
  2
  3const ChunkParser = @This();
  4const std = @import("std");
  5
  6state: State,
  7chunk_len: u64,
  8
  9pub const init: ChunkParser = .{
 10    .state = .head_size,
 11    .chunk_len = 0,
 12};
 13
 14pub const State = enum {
 15    head_size,
 16    head_ext,
 17    head_r,
 18    data,
 19    data_suffix,
 20    data_suffix_r,
 21    invalid,
 22};
 23
 24/// Returns the number of bytes consumed by the chunk size. This is always
 25/// less than or equal to `bytes.len`.
 26///
 27/// After this function returns, `chunk_len` will contain the parsed chunk size
 28/// in bytes when `state` is `data`. Alternately, `state` may become `invalid`,
 29/// indicating a syntax error in the input stream.
 30///
 31/// If the amount returned is less than `bytes.len`, the parser is in the
 32/// `chunk_data` state and the first byte of the chunk is at `bytes[result]`.
 33///
 34/// Asserts `state` is neither `data` nor `invalid`.
 35pub fn feed(p: *ChunkParser, bytes: []const u8) usize {
 36    for (bytes, 0..) |c, i| switch (p.state) {
 37        .data_suffix => switch (c) {
 38            '\r' => p.state = .data_suffix_r,
 39            '\n' => p.state = .head_size,
 40            else => {
 41                p.state = .invalid;
 42                return i;
 43            },
 44        },
 45        .data_suffix_r => switch (c) {
 46            '\n' => p.state = .head_size,
 47            else => {
 48                p.state = .invalid;
 49                return i;
 50            },
 51        },
 52        .head_size => {
 53            const digit = switch (c) {
 54                '0'...'9' => |b| b - '0',
 55                'A'...'Z' => |b| b - 'A' + 10,
 56                'a'...'z' => |b| b - 'a' + 10,
 57                '\r' => {
 58                    p.state = .head_r;
 59                    continue;
 60                },
 61                '\n' => {
 62                    p.state = .data;
 63                    return i + 1;
 64                },
 65                else => {
 66                    p.state = .head_ext;
 67                    continue;
 68                },
 69            };
 70
 71            const new_len = p.chunk_len *% 16 +% digit;
 72            if (new_len <= p.chunk_len and p.chunk_len != 0) {
 73                p.state = .invalid;
 74                return i;
 75            }
 76
 77            p.chunk_len = new_len;
 78        },
 79        .head_ext => switch (c) {
 80            '\r' => p.state = .head_r,
 81            '\n' => {
 82                p.state = .data;
 83                return i + 1;
 84            },
 85            else => continue,
 86        },
 87        .head_r => switch (c) {
 88            '\n' => {
 89                p.state = .data;
 90                return i + 1;
 91            },
 92            else => {
 93                p.state = .invalid;
 94                return i;
 95            },
 96        },
 97        .data => unreachable,
 98        .invalid => unreachable,
 99    };
100    return bytes.len;
101}
102
103test feed {
104    const testing = std.testing;
105
106    const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";
107
108    var p = init;
109    const first = p.feed(data[0..]);
110    try testing.expectEqual(@as(u32, 4), first);
111    try testing.expectEqual(@as(u64, 0xff), p.chunk_len);
112    try testing.expectEqual(.data, p.state);
113
114    p = init;
115    const second = p.feed(data[first..]);
116    try testing.expectEqual(@as(u32, 13), second);
117    try testing.expectEqual(@as(u64, 0xf0f000), p.chunk_len);
118    try testing.expectEqual(.data, p.state);
119
120    p = init;
121    const third = p.feed(data[first + second ..]);
122    try testing.expectEqual(@as(u32, 3), third);
123    try testing.expectEqual(@as(u64, 0), p.chunk_len);
124    try testing.expectEqual(.data, p.state);
125
126    p = init;
127    const fourth = p.feed(data[first + second + third ..]);
128    try testing.expectEqual(@as(u32, 16), fourth);
129    try testing.expectEqual(@as(u64, 0xffffffffffffffff), p.chunk_len);
130    try testing.expectEqual(.invalid, p.state);
131}