Changes
1 changed files (+410/-12)
-
-
@@ -235,6 +235,237 @@ try std.testing.expectError(Metadata.ParseError.NonUtf8ServiceName, err);} } const HeaderIterator = struct { buffer: []const u8, pub const IterateError = error{ NoHeaderDelimiterError, EmptyHeaderKey, NonUtf8HeaderKey, NonUtf8HeaderValue, }; /// Returns bytes read when a header is read, `key` and `value` is populated. /// Returns `null` when the buffer is exhausted, `key` and `value` is untouched. pub fn iterate(self: *HeaderIterator, key: *[]const u8, value: *[]const u8) IterateError!?usize { if (self.buffer.len == 0) { return null; } const line_end = std.mem.indexOfScalar(u8, self.buffer, '\n') orelse self.buffer.len; const line = self.buffer[0..line_end]; if (line.len == 0) { return null; } const delimiter = std.mem.indexOfScalar(u8, self.buffer, ':') orelse return IterateError.NoHeaderDelimiterError; const key_slice = self.buffer[0..delimiter]; if (key_slice.len == 0) { return IterateError.EmptyHeaderKey; } if (!std.unicode.utf8ValidateSlice(key_slice)) { return IterateError.NonUtf8HeaderKey; } const value_slice = std.mem.trimLeft(u8, self.buffer[delimiter + 1 .. line_end], " "); if (!std.unicode.utf8ValidateSlice(value_slice)) { return IterateError.NonUtf8HeaderKey; } key.* = key_slice; value.* = value_slice; const read = @min(line_end + 1, self.buffer.len); self.buffer = self.buffer[read..]; return read; } }; test "(HeaderIterator).iterate should parse a header line" { var iter = HeaderIterator{ .buffer = "Foo: Bar\n" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(9, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("Bar", bar); var key: []const u8 = undefined; var val: []const u8 = undefined; const second_read = try iter.iterate(&key, &val); try std.testing.expectEqual(null, second_read); } test "(HeaderIterator).iterate should parse header lines" { var iter = HeaderIterator{ .buffer = "Foo: foo-value\nbar: bar-value\nbaZ: baz-value\n" }; var foo_key: []const u8 = undefined; var foo_value: []const u8 = undefined; const foo_read = try iter.iterate(&foo_key, &foo_value); try std.testing.expectEqual(15, foo_read); try std.testing.expectEqualStrings("Foo", foo_key); try std.testing.expectEqualStrings("foo-value", foo_value); var bar_key: []const u8 = undefined; var bar_value: []const u8 = undefined; const bar_read = try iter.iterate(&bar_key, &bar_value); try std.testing.expectEqual(15, bar_read); try std.testing.expectEqualStrings("bar", bar_key); try std.testing.expectEqualStrings("bar-value", bar_value); var baz_key: []const u8 = undefined; var baz_value: []const u8 = undefined; const baz_read = try iter.iterate(&baz_key, &baz_value); try std.testing.expectEqual(15, baz_read); try std.testing.expectEqualStrings("baZ", baz_key); try std.testing.expectEqualStrings("baz-value", baz_value); var key: []const u8 = undefined; var val: []const u8 = undefined; const final_read = try iter.iterate(&key, &val); try std.testing.expectEqual(null, final_read); } test "(HeaderIterator).iterate should parse a header line terminated without newline" { var iter = HeaderIterator{ .buffer = "Foo: Bar" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(8, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("Bar", bar); var key: []const u8 = undefined; var val: []const u8 = undefined; const second_read = try iter.iterate(&key, &val); try std.testing.expectEqual(null, second_read); } test "(HeaderIterator).iterate should return false for blank inputs" { { var iter = HeaderIterator{ .buffer = "" }; var key: []const u8 = undefined; var val: []const u8 = undefined; const read = try iter.iterate(&key, &val); try std.testing.expectEqual(null, read); } { var iter = HeaderIterator{ .buffer = "\n" }; var key: []const u8 = undefined; var val: []const u8 = undefined; const read = try iter.iterate(&key, &val); try std.testing.expectEqual(null, read); } { var iter = HeaderIterator{ .buffer = "\n\n\n" }; var key: []const u8 = undefined; var val: []const u8 = undefined; const read = try iter.iterate(&key, &val); try std.testing.expectEqual(null, read); } } test "(HeaderIterator).iterate should return error for empty key" { // Only zero-length slice: the official Node.js API parses space-only headers... var iter = HeaderIterator{ .buffer = ": Value" }; var key: []const u8 = undefined; var val: []const u8 = undefined; try std.testing.expectError( HeaderIterator.IterateError.EmptyHeaderKey, iter.iterate(&key, &val), ); } test "(HeaderIterator).iterate should trim starting spaces" { // Many spaces { var iter = HeaderIterator{ .buffer = "Foo: Bar" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(19, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("Bar", bar); } // Zero space { var iter = HeaderIterator{ .buffer = "Foo:Bar" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(7, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("Bar", bar); } // Tab is not a space { var iter = HeaderIterator{ .buffer = "Foo:\tBar" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(8, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("\tBar", bar); } // Newline is not a space { var iter = HeaderIterator{ .buffer = "Foo:\nBar" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(5, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("", bar); } // NULL character is not a space { var iter = HeaderIterator{ .buffer = "Foo:\x00Bar" }; var foo: []const u8 = undefined; var bar: []const u8 = undefined; const read = try iter.iterate(&foo, &bar); try std.testing.expectEqual(8, read); try std.testing.expectEqualStrings("Foo", foo); try std.testing.expectEqualStrings("\x00Bar", bar); } } const BodyParsingContext = struct { start_position: usize,
-
@@ -254,19 +485,60 @@ /// Byte size of the body.content_length: usize, /// An ID unique to a connection, to associate corresponding request and response. /// This slice refers to the source message bytes. Accessing this field after /// free-ing the source message bytes is use-after-free. request_id: []const u8, request_id: i64, body_start_position: usize, pub const ParseError = error{}; pub const ParseError = error{ EmptyContentType, NonUintContentLength, NonIntRequestID, MissingContentType, MissingContentLength, MissingRequestID, } || HeaderIterator.IterateError; pub fn parse(message: []const u8, context: HeaderParsingContext) ParseError!@This() { _ = message; _ = context; var iter = HeaderIterator{ .buffer = message[context.start_position..] }; @panic("Not implemented"); var content_type: ?[]const u8 = null; var content_length: ?usize = null; var request_id: ?i64 = null; var i = context.start_position; var key: []const u8 = undefined; var val: []const u8 = undefined; while (try iter.iterate(&key, &val)) |read| { i += read; // Unlike HTTP headers, MOO headers are case-sensitive. if (std.mem.eql(u8, "Content-Type", key)) { if (val.len == 0) { return ParseError.EmptyContentType; } content_type = val; continue; } if (std.mem.eql(u8, "Content-Length", key)) { content_length = std.fmt.parseInt(usize, val, 10) catch return ParseError.NonUintContentLength; continue; } if (std.mem.eql(u8, "Request-Id", key)) { request_id = std.fmt.parseInt(i64, val, 10) catch return ParseError.NonIntRequestID; continue; } } return WellKnownHeaders{ .content_type = content_type orelse return ParseError.MissingContentType, .content_length = content_length orelse return ParseError.MissingContentLength, .request_id = request_id orelse return ParseError.MissingRequestID, .body_start_position = i, }; } pub fn getContext(self: *const @This()) BodyParsingContext {
-
@@ -278,6 +550,86 @@ };} }; test "WellKnownHeaders.parse should parse well-known headers" { const message = "MOO/1 REQUEST foo\nContent-Type: application/json; charset=utf-8\nContent-Length: 876\nRequest-Id: 1\n"; const meta = try Metadata.parse(message); const header = try WellKnownHeaders.parse(message, meta.context); try std.testing.expectEqualStrings("application/json; charset=utf-8", header.content_type); try std.testing.expectEqual(876, header.content_length); try std.testing.expectEqual(1, header.request_id); const ctx = header.getContext(); try std.testing.expectEqual(message.len, ctx.start_position); } test "WellKnownHeaders.parse should return error when required header is missing" { { const message = "MOO/1 REQUEST foo\nContent-Length: 876\nRequest-Id: 1\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.MissingContentType, WellKnownHeaders.parse(message, meta.context), ); } { const message = "MOO/1 REQUEST foo\nContent-Type: application/json; charset=utf-8\nRequest-Id: 1\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.MissingContentLength, WellKnownHeaders.parse(message, meta.context), ); } { const message = "MOO/1 REQUEST foo\nContent-Type: application/json; charset=utf-8\nContent-Length: 876\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.MissingRequestID, WellKnownHeaders.parse(message, meta.context), ); } } test "WellKnownHeaders.parse should return error on an unexpected format" { { const message = "MOO/1 REQUEST foo\nContent-Type:\nContent-Length: 1\nRequest-Id: 1\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.EmptyContentType, WellKnownHeaders.parse(message, meta.context), ); } { const message = "MOO/1 REQUEST foo\nContent-Type: text/plain\nContent-Length: 0x87\nRequest-Id: 1\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.NonUintContentLength, WellKnownHeaders.parse(message, meta.context), ); } { const message = "MOO/1 REQUEST foo\nContent-Type: text/plain\nContent-Length: one\nRequest-Id: 1\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.NonUintContentLength, WellKnownHeaders.parse(message, meta.context), ); } { const message = "MOO/1 REQUEST foo\nContent-Type: text/plain\nContent-Length: 1\nRequest-Id: 0D8C-F91C-22BB\n"; const meta = try Metadata.parse(message); try std.testing.expectError( WellKnownHeaders.ParseError.NonIntRequestID, WellKnownHeaders.parse(message, meta.context), ); } } /// HashMapHeaders pub const HashMapHeaders = struct { /// Header key-values.
-
@@ -287,20 +639,66 @@ map: std.hash_map.StringHashMap([]const u8),context: BodyParsingContext, pub const ParseError = std.mem.Allocator.Error; pub const ParseError = error{ MissingContentType, MissingContentLength, EmptyContentType, NonUintContentLength, } || HeaderIterator.IterateError || std.mem.Allocator.Error; pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: HeaderParsingContext) ParseError!@This() { _ = std.hash_map.StringHashMap([]const u8).init(allocator); _ = message; _ = context; var map = std.hash_map.StringHashMap([]const u8).init(allocator); var iter = HeaderIterator{ .buffer = message[context.start_position..] }; var i = context.start_position; var content_type: ?[]const u8 = null; var content_length: ?usize = null; @panic("Not implemented"); var key: []const u8 = undefined; var val: []const u8 = undefined; while (try iter.iterate(&key, &val)) |read| { i += read; if (std.mem.eql(u8, "Content-Type", key)) { if (val.len == 0) { return ParseError.EmptyContentType; } content_type = val; } else if (std.mem.eql(u8, "Content-Length", key)) { content_length = std.fmt.parseInt(usize, val, 10) catch return ParseError.NonUintContentLength; } try map.put(key, val); } return HashMapHeaders{ .map = map, .context = .{ .content_type = content_type orelse return ParseError.MissingContentType, .content_length = content_length orelse return ParseError.MissingContentLength, .start_position = i, }, }; } pub fn deinit(self: *@This()) void { self.map.deinit(); } }; test "HashMapHeaders.parse should save every headers" { const message = "MOO/1 REQUEST foo\nContent-Type: application/json; charset=utf-8\nContent-Length: 876\nRequest-Id: 1\nFoo: Bar\n"; const meta = try Metadata.parse(message); var header = try HashMapHeaders.parse(std.testing.allocator, message, meta.context); defer header.deinit(); try std.testing.expectEqualStrings(header.map.get("Content-Type").?, "application/json; charset=utf-8"); try std.testing.expectEqualStrings(header.map.get("Content-Length").?, "876"); try std.testing.expectEqualStrings(header.map.get("Request-Id").?, "1"); try std.testing.expectEqualStrings(header.map.get("Foo").?, "Bar"); try std.testing.expectEqual(message.len, header.context.start_position); } /// RawBody contains a bytes slice of body section in a MOO message. /// The bytes field points to the source message bytes: freeing the source message bytes
-