Changes
1 changed files (+222/-6)
-
-
@@ -557,13 +557,12 @@ /// Context required for body parsing and validation.const BodyParsingContext = struct { start_position: usize, content_type: []const u8, content_length: usize, content_type: ?[]const u8, content_length: ?usize, }; /// WellKnownHeaders stores headers defined in the official node-roon api source code and /// discards every other header fields. /// TODO: Make content_type and content_length optional or create headers for body-less message. pub const WellKnownHeaders = struct { /// MIME type (`Content-Type` header). content_type: []const u8,
-
@@ -812,6 +811,145 @@ \\, buf); } pub const NoBodyHeaders = struct { /// An ID unique to a connection, to associate corresponding request and response. /// (`Request-Id` header) request_id: i64, pub const ParseError = error{ NonIntRequestID, MissingRequestID, } || HeaderIterator.IterateError; /// Tries to parse the `message` bytes as MOO message, returns headers section and /// parser context for body parsing. /// **Slices in the returned struct are pointer for the input `message`**. /// Freeing `message` then accessing slice fields is use-after-free. /// /// This function returns a tuple of the parsed headers and context for body parsing. /// Pass the second one to a body struct's `parse` function. /// /// ```zig /// const metadata, const headers_ctx = try Metadata.parse(message); /// const headers, const body_ctx = try NoBodyHeaders.parse(message, headers_ctx); /// /// const body = try RawBody.parse(message, body_ctx); /// /// _ = metadata; /// _ = headers; /// _ = body; /// ``` pub fn parse(message: []const u8, context: HeaderParsingContext) ParseError!struct { @This(), BodyParsingContext } { var iter = HeaderIterator{ .buffer = message[context.start_position..] }; var i = context.start_position; var key: []const u8 = undefined; var val: []const u8 = undefined; while (try iter.iterate(&key, &val)) |read| { i += read; if (std.mem.eql(u8, "Request-Id", key)) { const request_id = std.fmt.parseInt(i64, val, 10) catch return ParseError.NonIntRequestID; return .{ NoBodyHeaders{ .request_id = request_id, }, BodyParsingContext{ // `i` does not include a final newline. .start_position = i + 1, .content_length = null, .content_type = null, }, }; } } return ParseError.MissingRequestID; } const fmt = \\Request-Id: {d} \\ \\ ; /// Returns how many bytes required for encoding. /// /// Mainly used for allocating an output buffer. /// /// ```zig /// const metadata = Metadata{ /// .verb = "REQUEST", /// .service = "com.example:1/hello", /// }; /// /// const headers = NoBodyHeaders{ /// .request_id = 1, /// }; /// /// const buf = try allocator.alloc( /// u8, /// metadata.getEncodeSize() + headers.getEncodeSize(), /// ); /// defer allocator.free(buf); /// var fbs = std.io.fixedBufferStream(buf); /// const writer = fbs.writer(); /// try metadata.encode(writer); /// try headers.encode(writer); /// ``` pub fn getEncodeSize(self: *const @This()) usize { return std.fmt.count(fmt, .{self.request_id}); } /// Encodes headers to bytes and writes to `writer`. /// /// `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.request_id}); } }; test "NoBodyHeaders.parse should parse well-known headers" { const message = "MOO/1 REQUEST foo\nRequest-Id: 1\n\n"; _, const meta_ctx = try Metadata.parse(message); const header, const header_ctx = try NoBodyHeaders.parse(message, meta_ctx); try std.testing.expectEqual(1, header.request_id); try std.testing.expectEqual(message.len, header_ctx.start_position); } test "NoBodyHeaders.parse should return error when required header is missing" { const message = "MOO/1 REQUEST foo\n\n"; _, const meta_ctx = try Metadata.parse(message); try std.testing.expectError( NoBodyHeaders.ParseError.MissingRequestID, NoBodyHeaders.parse(message, meta_ctx), ); } test "NoBodyHeaders.encodeInto" { const header = NoBodyHeaders{ .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( \\Request-Id: 99 \\ \\ , buf); } /// HashMapHeaders stores headers in a string-key-string-value hash map. pub const HashMapHeaders = struct { /// Hash map containing the headers.
-
@@ -997,12 +1135,55 @@ );try std.testing.expectStringEndsWith(buf, "\n\n"); } /// NoBody indicates a message has no body. /// /// While you can use empty `RawBody` to achieve the same goal, this has benefits over `RawBody`: /// * At parse time, this struct checks there is no body. /// * Does not emit `content-type` and `content-length` headers. /// * More space efficient (1 byte less). pub const NoBody = struct { pub const ParseError = error{ UnexpectedBodyPart, }; /// Validates if body is empty and returns `NoBody`. pub fn parse(message: []const u8, context: BodyParsingContext) ParseError!@This() { const start = @min(context.start_position, message.len); if (start != message.len - 1) { return ParseError.UnexpectedBodyPart; } return .{}; } /// Returns 0. /// Being here for consistency to other body structs. pub fn getEncodeSize(_: *const @This()) usize { return 0; } /// Does nothing. /// Being here for consistency to other body structs. pub fn encode(_: *const @This(), writer: anytype) @TypeOf(writer).Error!void { return; } /// Get minimal header for the message. /// Handy for constructing MOO message. pub fn getHeader(_: *const @This(), request_id: i64) NoBodyHeaders { return NoBodyHeaders{ .request_id = request_id, }; } }; /// RawBody simply is bytes for body section in a MOO message. pub const RawBody = struct { /// Bytes for body secion in a MOO message. bytes: []const u8, pub const ParseError = error{ MissingContentLength, ContentLengthMismatch, };
-
@@ -1010,10 +1191,12 @@ /// Validates bytes length and returns fulfilled `RawBody` if the size matches./// **Slices in the returned struct are pointer for the input `message`**. /// Freeing `message` then accessing slice fields is use-after-free. pub fn parse(message: []const u8, context: BodyParsingContext) ParseError!@This() { const content_length = context.content_length orelse return ParseError.MissingContentLength; const start = @min(context.start_position, message.len); const bytes = message[start..]; if (bytes.len != context.content_length) { if (bytes.len != content_length) { return ParseError.ContentLengthMismatch; }
-
@@ -1097,6 +1280,8 @@ /// Body data. Do not access after `deinit()`.value: *const T, pub const ParseError = error{ MissingContentType, MissingContentLength, ContentLengthMismatch, ContentTypeIsNotApplicationJson, } || std.mem.Allocator.Error || std.json.ParseError(std.json.Scanner);
-
@@ -1108,17 +1293,20 @@ /// input message bytes. This depends on how `std.json.parseFromSliceLeaky`./// /// Caller owns the returned struct. Call `deinit()` once it's no longer in use. pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: BodyParsingContext) ParseError!@This() { const content_type = context.content_type orelse return ParseError.MissingContentType; const content_length = context.content_length orelse return ParseError.MissingContentLength; // Same MIME checking as one in the official Node.js API. // This does not match to "application/json; charset=utf-8" or such. // (not doing the strict MIME check because parsing MIME is complex task) if (!std.mem.eql(u8, "application/json", context.content_type)) { if (!std.mem.eql(u8, "application/json", content_type)) { return ParseError.ContentTypeIsNotApplicationJson; } const start = @min(context.start_position, message.len); const bytes = message[start..]; if (bytes.len != context.content_length) { if (bytes.len != content_length) { return ParseError.ContentLengthMismatch; }
-
@@ -1383,3 +1571,31 @@ \\\\{"id":"alice"} , buf); } test "No body encode" { const meta = Metadata{ .version = 1, .verb = "GET", .service = "user/account", }; const body = NoBody{}; const header = body.getHeader(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 \\Request-Id: 1 \\ \\ , buf); }
-