Changes
1 changed files (+267/-8)
-
-
@@ -119,6 +119,18 @@ .start_position = line_delimiter_position + 1,}, }; } const fmt = signature ++ "{d} {s} {s}\n"; pub fn getEncodeSize(self: *const @This()) usize { return std.fmt.count(fmt, .{ self.version, self.verb, self.service }); } /// `writer` must be `std.io.GenericWriter`. /// Error set is inferred due to `std.fmt.format` not having explicit error set. pub fn encode(self: *const @This(), writer: anytype) !void { try std.fmt.format(writer, fmt, .{ self.version, self.verb, self.service }); } }; test Metadata {
-
@@ -232,6 +244,30 @@{ const err = Metadata.parse("MOO/1 REQUEST \xc3\x28\n"); try std.testing.expectError(Metadata.ParseError.NonUtf8ServiceName, err); } } test "Metadata.encodeInto" { { const meta = Metadata{ .version = 8, .verb = "EAT", .service = "creature/dog", }; const size = meta.getEncodeSize(); const buf: []u8 = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try meta.encode(fbs.writer()); try std.testing.expectEqualStrings("MOO/8 EAT creature/dog\n", buf); const parsed, _ = try Metadata.parse(buf); try std.testing.expectEqual(meta.version, parsed.version); try std.testing.expectEqualStrings(meta.verb, parsed.verb); try std.testing.expectEqualStrings(meta.service, parsed.service); } }
-
@@ -545,6 +581,32 @@ .content_length = content_length.?,}, }; } const fmt = \\Content-Type: {s} \\Content-Length: {d} \\Request-Id: {d} \\ \\ ; pub fn getEncodeSize(self: *const WellKnownHeaders) usize { return std.fmt.count(fmt, .{ self.content_type, self.content_length, self.request_id, }); } /// `writer` must be `std.io.GenericWriter`. /// Error set is inferred due to `std.fmt.format` not having explicit error set. pub fn encode(self: *const WellKnownHeaders, writer: anytype) !void { try std.fmt.format(writer, fmt, .{ self.content_type, self.content_length, self.request_id, }); } }; test "WellKnownHeaders.parse should parse well-known headers" {
-
@@ -626,6 +688,29 @@ );} } test "WellknownHeaders.encodeInto" { const header = WellKnownHeaders{ .content_type = "text/plain; charset=utf-8", .content_length = 2551, .request_id = 99, }; const size = header.getEncodeSize(); const buf: []u8 = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try header.encode(fbs.writer()); try std.testing.expectEqualStrings( \\Content-Type: text/plain; charset=utf-8 \\Content-Length: 2551 \\Request-Id: 99 \\ \\ , buf); } /// HashMapHeaders pub const HashMapHeaders = struct { /// Header key-values.
-
@@ -682,6 +767,36 @@pub fn deinit(self: *@This()) void { self.map.deinit(); } pub fn init(allocator: std.mem.Allocator) HashMapHeaders { const map = std.hash_map.StringHashMap([]const u8).init(allocator); return HashMapHeaders{ .map = map }; } const line_fmt = "{s}: {s}\n"; pub fn getEncodeSize(self: *const HashMapHeaders) usize { var size: usize = 0; var iter = self.map.iterator(); while (iter.next()) |entry| { size += std.fmt.count(line_fmt, .{ entry.key_ptr.*, entry.value_ptr.* }); } return size + 1; } /// `writer` must be `std.io.GenericWriter`. /// Error set is inferred due to `std.fmt.format` not having explicit error set. pub fn encode(self: *const HashMapHeaders, writer: anytype) !void { var iter = self.map.iterator(); while (iter.next()) |entry| { try std.fmt.format(writer, line_fmt, .{ entry.key_ptr.*, entry.value_ptr.*, }); } _ = try writer.write("\n"); } }; test "HashMapHeaders.parse should save every headers" {
-
@@ -697,6 +812,37 @@ try std.testing.expectEqualStrings(header.map.get("Foo").?, "Bar");try std.testing.expectEqual(message.len, header_ctx.start_position); } test "HashMapHeaders.encodeInto" { var header = HashMapHeaders.init(std.testing.allocator); defer header.deinit(); try header.map.put("Content-Type", "text/plain; charset=utf-8"); try header.map.put("Content-Length", "2551"); try header.map.put("Request-Id", "99"); try header.map.put("X-Foo", "Bar"); const size = header.getEncodeSize(); const buf: []u8 = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try header.encode(fbs.writer()); try std.testing.expect( std.mem.indexOf(u8, buf, "Content-Type: text/plain; charset=utf-8\n").? >= 0, ); try std.testing.expect( std.mem.indexOf(u8, buf, "Content-Length: 2551\n").? >= 0, ); try std.testing.expect( std.mem.indexOf(u8, buf, "Request-Id: 99\n").? >= 0, ); try std.testing.expect( std.mem.indexOf(u8, buf, "X-Foo: Bar\n").? >= 0, ); try std.testing.expectStringEndsWith(buf, "\n\n"); } /// 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 /// then accessing the bytes field results in use-after-free.
-
@@ -717,6 +863,15 @@ }return .{ .bytes = bytes }; } pub fn getEncodeSize(self: *const @This()) usize { return self.bytes.len; } /// `writer` must be `std.io.GenericWriter`. pub fn encode(self: *const @This(), writer: anytype) @TypeOf(writer).Error!void { try writer.writeAll(self.bytes); } }; test "RawBody.parse should save rest of the bytes" {
-
@@ -748,18 +903,30 @@ try std.testing.expectError(RawBody.ParseError.ContentLengthMismatch, err);} } test "RawBody.encodeInto" { const body = RawBody{ .bytes = "Foo Bar" }; const size = body.getEncodeSize(); const buf: []u8 = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try body.encode(fbs.writer()); try std.testing.expectEqualStrings("Foo Bar", buf); } /// JsonBody stores MOO message body as JSON value. User has to provided an expected /// type (schema) as "T". pub fn JsonBody(comptime T: type) type { return struct { allocator: std.mem.Allocator, arena: ?*std.heap.ArenaAllocator = null, value: *const T, parsed: std.json.Parsed(T), pub const ParseError = error{ ContentLengthMismatch, ContentTypeIsNotApplicationJson, } || std.json.ParseError(std.json.Scanner); } || std.mem.Allocator.Error || std.json.ParseError(std.json.Scanner); pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: BodyParsingContext) ParseError!@This() { // Same MIME checking as one in the official Node.js API.
-
@@ -776,17 +943,41 @@ if (bytes.len != context.content_length) {return ParseError.ContentLengthMismatch; } const parsed = try std.json.parseFromSlice(T, allocator, bytes, .{}); var arena = try allocator.create(std.heap.ArenaAllocator); errdefer allocator.destroy(arena); arena.* = std.heap.ArenaAllocator.init(allocator); errdefer arena.deinit(); const arena_allocator = arena.allocator(); const parsed = try std.json.parseFromSliceLeaky(T, arena_allocator, bytes, .{}); return .{ .allocator = allocator, .value = &parsed.value, .parsed = parsed, .arena = arena, .value = &parsed, }; } pub fn deinit(self: *const @This()) void { self.parsed.deinit(); if (self.arena) |arena| { const allocator = arena.child_allocator; arena.deinit(); allocator.destroy(arena); } } pub fn init(value: *const T) @This() { return .{ .value = value }; } pub fn getEncodeSize(self: *const @This()) usize { var cw = std.io.countingWriter(std.io.null_writer); std.json.stringify(self.value, .{}, cw.writer()) catch return 0; return cw.bytes_written; } pub fn encode(self: *const @This(), writer: anytype) @TypeOf(writer).Error!void { try std.json.stringify(self.value, .{}, writer); } }; }
-
@@ -895,3 +1086,71 @@ const err = JsonBody(TestData).parse(std.testing.allocator, message, header_ctx);try std.testing.expectError(JsonBody(TestData).ParseError.ContentTypeIsNotApplicationJson, err); } test "JsonBody.encodeInto" { const TestData = struct { foo: i32, }; const body = JsonBody(TestData){ .value = &TestData{ .foo = 29 }, }; const size = body.getEncodeSize(); const buf: []u8 = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try body.encode(fbs.writer()); try std.testing.expectEqualStrings("{\"foo\":29}", buf); } /// Writes encoded MOO message. /// /// `writer` must be `std.io.GenericWriter`. /// `header` must have `.encode(writer: anytype) !void`. /// `body` must have `.encode(writer: anytype) !void`. pub fn encode(writer: anytype, meta: Metadata, header: anytype, body: anytype) !void { try meta.encode(writer); try header.encode(writer); try body.encode(writer); } test encode { const UserInfo = struct { id: []const u8, }; const meta = Metadata{ .version = 1, .verb = "GET", .service = "user/account", }; const body = JsonBody(UserInfo).init(&UserInfo{ .id = "alice" }); const header = WellKnownHeaders{ .content_type = "application/json", .content_length = body.getEncodeSize(), .request_id = 1, }; const message_size = meta.getEncodeSize() + header.getEncodeSize() + body.getEncodeSize(); const buf = try std.testing.allocator.alloc(u8, message_size); defer std.testing.allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); const writer = fbs.writer(); try encode(writer, meta, header, body); try std.testing.expectEqualStrings( \\MOO/1 GET user/account \\Content-Type: application/json \\Content-Length: 14 \\Request-Id: 1 \\ \\{"id":"alice"} , buf); }
-