Changes
1 changed files (+226/-15)
-
-
@@ -23,16 +23,36 @@ //! Typical MOO message have "Content-Type" header that describes how the message's body//! is encoded. (e.g. "application/json") //! The first line of MOO message is metadata line, which is in "MOO/VERSION VERB SERVICE" //! format. SERVICE is delimited by slash. //! //! ## Decoding //! //! Parse functions takes a whole buffer (`[]const u8`) and returns a result. To reduce //! copy, returned structs' slice fields are pointer for a part of the input buffer. //! **Accessing those field after freeing the input buffer is use-after-free**. //! Even those structs that take `std.mem.Allocator` as a parameter, does not copy the //! underlying buffer. //! //! Parse functions are named `parse()` but might be changed to `decode()` in a future. //! //! ## Encoding //! //! To build MOO message bytes from structs, pass a writer to `.encode()` methods. //! //! Each structs has `getEncodeSize(self: *const @This()) usize` function for buffer //! allocation: allocate `[]u8` of the size of sum of `getEncodeSize` result, then //! create `std.io.FixedBufferStream` from that. //! Most of the time, this simple buffer creation would be enough. // TODO: Move tests for public APIs out of this file. "lib_test.zig" or such. const std = @import("std"); /// Context required for parsing headers section. const HeaderParsingContext = struct { start_position: usize, }; /// Metadata contains a MOO message's version and semantics. /// Fields point to the source message bytes: freeing the source message bytes then /// accessing fields results in use-after-free. pub const Metadata = struct { /// Message schema version described in metadata line. version: u32 = 1,
-
@@ -54,6 +74,20 @@ InvalidService,NonUtf8ServiceName, }; /// Tries to parse the `message` bytes as MOO message, returns metadata section and /// parser context for header 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 metadata and context for header parsing. /// Pass the second one to headers struct's `parse` function. /// /// ```zig /// const metadata, const headers_ctx = try Metadata.parse(message); /// /// _, _ = try WellKnownHeaders.parse(message, headers_ctx); /// _, _ = try HashmapHeaders.parse(allocator, message, headers_ctx); /// ``` pub fn parse(message: []const u8) ParseError!struct { @This(), HeaderParsingContext } { var i: usize = 0;
-
@@ -122,10 +156,27 @@ }const fmt = signature ++ "{d} {s} {s}\n"; /// 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 buf = try allocator.alloc(u8, metadata.getEncodeSize()); /// defer allocator.free(buf); /// var fbs = std.io.fixedBufferStream(buf); /// try meta.encode(fbs.writer()); /// ``` pub fn getEncodeSize(self: *const @This()) usize { return std.fmt.count(fmt, .{ self.version, self.verb, self.service }); } /// Encodes metadata 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 {
-
@@ -502,6 +553,7 @@ try std.testing.expectEqualStrings("\x00Bar", bar);} } /// Context required for body parsing and validation. const BodyParsingContext = struct { start_position: usize,
-
@@ -511,16 +563,16 @@ };/// 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. /// This slice refers to the source message bytes. Accessing this field after /// free-ing the source message bytes is use-after-free. /// MIME type (`Content-Type` header). content_type: []const u8, /// Byte size of the body. /// Byte size of the body (`Content-Length` header). content_length: usize, /// An ID unique to a connection, to associate corresponding request and response. /// (`Request-Id` header) request_id: i64, pub const ParseError = error{
-
@@ -532,6 +584,27 @@ MissingContentLength,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. /// /// Headers defined as a field will be ignored. /// If you want to read other headers, use `HashMapHeaders` instead. /// /// 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 WellKnownHeaders.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..] };
-
@@ -590,6 +663,32 @@ \\\\ ; /// 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 = WellKnownHeaders{ /// .content_type = "application/json", /// .content_length = 13, /// .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 WellKnownHeaders) usize { return std.fmt.count(fmt, .{ self.content_type,
-
@@ -598,6 +697,8 @@ 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 WellKnownHeaders, writer: anytype) !void {
-
@@ -711,11 +812,9 @@ \\, buf); } /// HashMapHeaders /// HashMapHeaders stores headers in a string-key-string-value hash map. pub const HashMapHeaders = struct { /// Header key-values. /// Both key and value points to the source message bytes. Accessing those after /// free-ing the source message bytes is use-after-free. /// Hash map containing the headers. map: std.hash_map.StringHashMap([]const u8), pub const ParseError = error{
-
@@ -725,6 +824,27 @@ EmptyContentType,NonUintContentLength, } || HeaderIterator.IterateError || std.mem.Allocator.Error; /// Tries to parse the `message` bytes as MOO message, returns headers section and /// parser context for body parsing. /// **Slices in the returned hash map are pointer for the input `message`**. /// Freeing `message` then accessing key or value is use-after-free. /// The `allocator` is used for managing hash map entries, not copying bytes. /// /// 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 HashMapHeaders.parse(allocator, message, headers_ctx); /// /// const body = try RawBody.parse(message, body_ctx); /// /// _ = metadata; /// _ = headers; /// _ = body; /// ``` /// /// Call `deinit()` once you no longer use the returned struct. pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: HeaderParsingContext) ParseError!struct { @This(), BodyParsingContext } { var map = std.hash_map.StringHashMap([]const u8).init(allocator); var iter = HeaderIterator{ .buffer = message[context.start_position..] };
-
@@ -764,10 +884,15 @@ },}; } /// Release the hash map. /// This does not invalidates keys and values. pub fn deinit(self: *@This()) void { self.map.deinit(); } /// Returns a newly created `HashMapHeaders`, for convenience. /// /// Call `deinit()` once you no longer use the returned struct. pub fn init(allocator: std.mem.Allocator) HashMapHeaders { const map = std.hash_map.StringHashMap([]const u8).init(allocator);
-
@@ -776,6 +901,30 @@ }const line_fmt = "{s}: {s}\n"; /// 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", /// }; /// /// var headers = HashMapHeaders.init(allocator); /// defer headers.deinit(); /// try headers.map.put("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 HashMapHeaders) usize { var size: usize = 0; var iter = self.map.iterator();
-
@@ -785,6 +934,11 @@ }return size + 1; } /// Encodes headers to bytes and writes to `writer`. /// /// The order of header lines are not guaranteed. /// You should not rely on the order. /// /// `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 {
-
@@ -843,16 +997,18 @@ );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. /// 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{ ContentLengthMismatch, }; /// 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 start = @min(context.start_position, message.len); const bytes = message[start..];
-
@@ -864,10 +1020,13 @@return .{ .bytes = bytes }; } /// Returns how many bytes required for encoding. pub fn getEncodeSize(self: *const @This()) usize { return self.bytes.len; } /// Write bytes to `writer`. /// /// `writer` must be `std.io.GenericWriter`. pub fn encode(self: *const @This(), writer: anytype) @TypeOf(writer).Error!void { try writer.writeAll(self.bytes);
-
@@ -916,11 +1075,15 @@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". /// JsonBody is a typed and structured body encoded to/decoded from JSON text. /// /// You have to pass JSON-de/serializable type to `T`. pub fn JsonBody(comptime T: type) type { return struct { /// Allocator manages `value`. arena: ?*std.heap.ArenaAllocator = null, /// Body data. Do not access after `deinit()`. value: *const T, pub const ParseError = error{
-
@@ -928,6 +1091,12 @@ ContentLengthMismatch,ContentTypeIsNotApplicationJson, } || std.mem.Allocator.Error || std.json.ParseError(std.json.Scanner); /// Tries to parse the `message` bytes as MOO message, parses body section as /// JSON using `T` as a schema, then returns the parsed data. /// Slices (strings) inside the retured data *might* be a pointer for the /// 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() { // Same MIME checking as one in the official Node.js API. // This does not match to "application/json; charset=utf-8" or such.
-
@@ -960,6 +1129,9 @@ .value = value,}; } /// Release `value` and temporary data made during JSON parsing. /// If this struct is initialized using `init()`, this function is /// no-op. pub fn deinit(self: *const @This()) void { if (self.arena) |arena| { const allocator = arena.child_allocator;
-
@@ -968,16 +1140,55 @@ allocator.destroy(arena);} } /// Creates and returns JSON encodable body. pub fn init(value: *const T) @This() { return .{ .value = value }; } /// 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 = WellKnownHeaders{ /// .content_type = "application/json", /// .content_length = 13, /// .request_id = 1, /// }; /// /// const MyData = struct { foo: i32 }; /// /// const body = JsonBody(MyData).init(&MyData{ .foo = 3 }); /// /// const buf = try allocator.alloc( /// u8, /// metadata.getEncodeSize() + headers.getEncodeSize() + body.getEncodeSize(), /// ); /// defer allocator.free(buf); /// var fbs = std.io.fixedBufferStream(buf); /// const writer = fbs.writer(); /// try metadata.encode(writer); /// try headers.encode(writer); /// try body.encode(writer); /// ``` /// /// This function serializes `value` to JSON text to compute required byte size. /// Use dynamic buffer (e.g. `std.ArrayList(u8)`) if the serialization cost /// matters. 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; } /// Serializes `value` to JSON text and writes it to `writer`. /// /// `writer` must be `std.io.GenericWriter`. pub fn encode(self: *const @This(), writer: anytype) @TypeOf(writer).Error!void { try std.json.stringify(self.value, .{}, writer); }
-