Changes
2 changed files (+250/-43)
-
-
@@ -17,6 +17,19 @@const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Exports Zig API. A project depending on this package imports this module. _ = b.addModule("moo", .{ .root_source_file = b.path("src/lib.zig") }); // Tests const test_step = b.step("test", "Run unit tests"); const unit_tests = b.addTest(.{ .name = "moo_test", .root_source_file = b.path("src/lib.zig"), .target = target, .optimize = optimize, }); test_step.dependOn(&b.addRunArtifact(unit_tests).step); }
-
-
-
@@ -26,20 +26,8 @@ //! format. SERVICE is delimited by slash.const std = @import("std"); pub const Parser = struct { state: State = .before_metadata, position: usize = 0, buffer: []const u8, content_type: ?[]const u8 = null, content_length: ?usize = null, const State = enum { before_metadata, before_header, in_header, before_body, done, }; const HeaderParsingContext = struct { start_position: usize, }; /// Metadata contains a MOO message's version and semantics.
-
@@ -56,61 +44,261 @@/// Service name described in metadata line. service: []const u8, context: HeaderParsingContext, const signature = "MOO/"; pub const ParseError = error{ BufferTooShort, InvalidSignature, InvalidVersionNumber, InvalidVerb, InvalidService, NonUtf8ServiceName, }; pub fn init(parser: Parser) ParseError!@This() { _ = parser; @panic("Not implemented"); pub fn parse(message: []const u8) ParseError!@This() { var i: usize = 0; if (!std.mem.startsWith(u8, message, signature)) { return ParseError.InvalidSignature; } i += signature.len; const version = if (std.mem.indexOfScalarPos(u8, message, i, ' ')) |space_position| blk: { if (i == space_position) { return ParseError.InvalidVersionNumber; } const parsed = std.fmt.parseInt(u32, message[i..space_position], 10) catch return ParseError.InvalidVersionNumber; i = space_position + 1; break :blk parsed; } else { return ParseError.InvalidVersionNumber; }; const verb = if (std.mem.indexOfScalarPos(u8, message, i, ' ')) |space_position| blk: { if (i == space_position) { return ParseError.InvalidVerb; } const s = message[i..space_position]; for (s) |char| { if (char < 'A' or char > 'Z') { return ParseError.InvalidVerb; } } i = space_position + 1; break :blk s; } else { return ParseError.InvalidVerb; }; const line_delimiter_position = std.mem.indexOfScalarPos(u8, message, i, '\n') orelse message.len; const service = message[i..line_delimiter_position]; if (service.len == 0) { return ParseError.InvalidService; } if (!std.unicode.utf8ValidateSlice(service)) { return ParseError.NonUtf8ServiceName; } return @This(){ .version = version, .verb = verb, .service = service, .context = HeaderParsingContext{ .start_position = line_delimiter_position + 1, }, }; } }; /// WellKnownFieldsHeader contains header fields appears in the official roon-node-api /// source code. /// Fields point to the source message bytes: freeing the source message bytes then /// accessing fields results in use-after-free. pub const WellKnownFieldsHeader = struct { test Metadata { { const metadata = try Metadata.parse("MOO/1 REQUEST foo/bar\n"); try std.testing.expectEqual(1, metadata.version); try std.testing.expectEqualStrings("REQUEST", metadata.verb); try std.testing.expectEqualStrings("foo/bar", metadata.service); } { const metadata = try Metadata.parse("MOO/20 COMPLETE foo\n"); try std.testing.expectEqual(20, metadata.version); try std.testing.expectEqualStrings("COMPLETE", metadata.verb); try std.testing.expectEqualStrings("foo", metadata.service); } } test "(Metadata).parse() should reject invalid signature" { { const err = Metadata.parse(""); try std.testing.expectError(Metadata.ParseError.InvalidSignature, err); } { const err = Metadata.parse("\n"); try std.testing.expectError(Metadata.ParseError.InvalidSignature, err); } { const err = Metadata.parse("MOO1 REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidSignature, err); } { const err = Metadata.parse("MEOW/1 REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidSignature, err); } } test "(Metadata).parse() should reject invalid version" { { const err = Metadata.parse("MOO/1.0 REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err); } { const err = Metadata.parse("MOO/-1 REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err); } { const err = Metadata.parse("MOO/ REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err); } { const err = Metadata.parse("MOO/0x1 REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err); } { const err = Metadata.parse("MOO/one REQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err); } } test "(Metadata).parse() should reject invalid verb" { { const err = Metadata.parse("MOO/1 request foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVerb, err); } { const err = Metadata.parse("MOO/1 RÉQUEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVerb, err); } { const err = Metadata.parse("MOO/1 REQUEST1 foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVerb, err); } { const err = Metadata.parse("MOO/1 RE❓️UEST foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVerb, err); } { const err = Metadata.parse("MOO/1 \n"); try std.testing.expectError(Metadata.ParseError.InvalidVerb, err); } { const err = Metadata.parse("MOO/1 foo/bar\n"); try std.testing.expectError(Metadata.ParseError.InvalidVerb, err); } } test "(Metadata).parse() should reject invalid service" { { const err = Metadata.parse("MOO/1 REQUEST \n"); try std.testing.expectError(Metadata.ParseError.InvalidService, err); } { const err = Metadata.parse("MOO/1 REQUEST "); try std.testing.expectError(Metadata.ParseError.InvalidService, err); } { const err = Metadata.parse("MOO/1 REQUEST \xc3\x28\n"); try std.testing.expectError(Metadata.ParseError.NonUtf8ServiceName, err); } } const BodyParsingContext = struct { start_position: 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. 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. content_type: []const u8, /// Byte size of the body. content_length: usize, /// An ID unique to a connection, which is used for identificating which /// response is for which request. /// 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, body_start_position: usize, pub const ParseError = error{}; pub fn init(parser: Parser) ParseError!@This() { _ = parser; pub fn parse(message: []const u8, context: HeaderParsingContext) ParseError!@This() { _ = message; _ = context; @panic("Not implemented"); } }; /// HashMapHeader stores header fields as a string hash map. /// Fields point to the source message bytes: freeing the source message bytes then /// accessing fields results in use-after-free. pub const HashMapHeader = struct { allocator: std.mem.Allocator, pub fn getContext(self: *const @This()) BodyParsingContext { return .{ .start_position = self.body_start_position, .content_type = self.content_type, .content_length = self.content_length, }; } }; /// HashMapHeaders 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. map: std.hash_map.StringHashMap([]const u8), pub const ParseError = error{}; context: BodyParsingContext, pub fn init(allocator: std.mem.Allocator, parser: Parser) ParseError!@This() { var map = std.hash_map.StringHashMap([]const u8).init(allocator); errdefer map.deinit(); pub const ParseError = 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; _ = parser; @panic("Not implemented"); } pub fn deinit(self: *@This()) void { self.map.deinit(); self.* = undefined; } };
-
@@ -120,8 +308,11 @@ /// then accessing the bytes field results in use-after-free.pub const RawBody = struct { bytes: []const u8, pub fn init(parser: Parser) @This() { _ = parser; pub const ParseError = error{}; pub fn parse(message: []const u8, context: BodyParsingContext) ParseError!@This() { _ = message; _ = context; @panic("Not implemented"); } };
-
@@ -134,9 +325,12 @@ allocator: std.mem.Allocator,value: *const T, parsed: std.json.Parsed(T), pub fn init(allocator: std.mem.Allocator, parser: Parser) @This() { pub const ParseError = error{}; pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: BodyParsingContext) ParseError!@This() { _ = allocator; _ = parser; _ = message; _ = context; @panic("Not implemented"); }
-