diff --git a/src/parser/flow.zig b/src/parser/flow.zig index 01d596bf..228b7b10 100644 --- a/src/parser/flow.zig +++ b/src/parser/flow.zig @@ -189,26 +189,28 @@ fn appendSequenceEntryEvents( return true; } - var key_events: EventBuilder = .{}; - defer key_events.deinit(allocator); - try key_events.ensureTotalCapacity(allocator, @min(end - index.*, 4)); - + const key_checkpoint = events.checkpoint(); + var key_committed = false; + errdefer if (!key_committed) events.rollback(key_checkpoint); const key_start = index.*; - if (!try appendNodeEventsWithOptions(allocator, tokens, index, end, &key_events, depth, directives, .{ + if (!try appendNodeEventsWithOptions(allocator, tokens, index, end, events, depth, directives, .{ .allow_plain_indented_continuations = true, - })) return false; + })) { + events.rollback(key_checkpoint); + return false; + } token_cursor.skipComments(tokens, index, end); if (token_cursor.flowMappingValueFollowsLineBreak(tokens, index.*, end)) return ParseError.InvalidSyntax; if (index.* >= end or tokens[index.*] != .flow_mapping_value) { - try events.appendSlice(allocator, key_events.slice()); + key_committed = true; return true; } try implicit_key.validateImplicitTokenKeyLength(tokens, key_start, index.*); index.* += 1; - try events.append(allocator, .{ .mapping_start = .{ .style = .flow } }); - try events.appendSlice(allocator, key_events.slice()); + try events.insert(allocator, key_checkpoint.len, .{ .mapping_start = .{ .style = .flow } }); + key_committed = true; token_cursor.skipFlowInsignificant(tokens, index, end); if (token_cursor.isFlowSequenceEntryBoundary(tokens, index.*, end)) { diff --git a/src/parser/internal.zig b/src/parser/internal.zig index 10054bc8..9cdc7fce 100644 --- a/src/parser/internal.zig +++ b/src/parser/internal.zig @@ -54,11 +54,26 @@ pub const EventBuilder = struct { list: std.ArrayList(Event) = .empty, stats: EventStats = .{}, + pub const Checkpoint = struct { + len: usize, + stats: EventStats, + }; + pub fn deinit(self: *EventBuilder, allocator: std.mem.Allocator) void { self.list.deinit(allocator); self.* = .{}; } + pub fn checkpoint(self: *const EventBuilder) Checkpoint { + return .{ .len = self.list.items.len, .stats = self.stats }; + } + + pub fn rollback(self: *EventBuilder, saved: Checkpoint) void { + std.debug.assert(saved.len <= self.list.items.len); + self.list.items.len = saved.len; + self.stats = saved.stats; + } + pub fn ensureTotalCapacity(self: *EventBuilder, allocator: std.mem.Allocator, capacity: usize) std.mem.Allocator.Error!void { try self.list.ensureTotalCapacity(allocator, capacity); } @@ -73,6 +88,11 @@ pub const EventBuilder = struct { self.stats.observeSlice(events); } + pub fn insert(self: *EventBuilder, allocator: std.mem.Allocator, index: usize, event: Event) std.mem.Allocator.Error!void { + try self.list.insert(allocator, index, event); + self.recomputeStats(); + } + pub fn toOwnedSlice(self: *EventBuilder, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const Event { return self.list.toOwnedSlice(allocator); } @@ -80,6 +100,11 @@ pub const EventBuilder = struct { pub fn slice(self: *const EventBuilder) []const Event { return self.list.items; } + + fn recomputeStats(self: *EventBuilder) void { + self.stats = .{}; + self.stats.observeSlice(self.list.items); + } }; test "event builder tracks event stats while appending" { @@ -101,6 +126,26 @@ test "event builder tracks event stats while appending" { try std.testing.expect(!builder.stats.malformed_nesting); } +test "event builder rollback restores events and stats" { + var builder: EventBuilder = .{}; + defer builder.deinit(std.testing.allocator); + + try builder.append(std.testing.allocator, .stream_start); + try builder.append(std.testing.allocator, .{ .sequence_start = .{ .style = .flow } }); + try builder.append(std.testing.allocator, .{ .scalar = .{ .value = "kept" } }); + const checkpoint = builder.checkpoint(); + + try builder.append(std.testing.allocator, .{ .mapping_start = .{ .style = .flow } }); + try builder.append(std.testing.allocator, .{ .scalar = .{ .value = "discarded" } }); + try builder.append(std.testing.allocator, .mapping_end); + builder.rollback(checkpoint); + + try std.testing.expectEqual(@as(usize, 3), builder.slice().len); + try std.testing.expectEqual(@as(usize, 3), builder.stats.event_count); + try std.testing.expectEqual(@as(usize, 4), builder.stats.max_scalar_bytes); + try std.testing.expectEqual(@as(usize, 1), builder.stats.current_nesting_depth); +} + pub const Line = struct { start: usize, end: usize, diff --git a/src/parser/parser.zig b/src/parser/parser.zig index 616822a5..5fd7a159 100644 --- a/src/parser/parser.zig +++ b/src/parser/parser.zig @@ -494,3 +494,17 @@ test "parseTokensWithStats reports private event stats" { try std.testing.expectEqual(@as(usize, 0), parsed.stats.current_nesting_depth); try std.testing.expect(!parsed.stats.malformed_nesting); } + +test "parseTokensWithStats reports flow sequence implicit mapping stats" { + var token_stream = try scanner.scan(std.testing.allocator, "[plain, key: [nested], tail]\n"); + defer token_stream.deinit(); + + var parsed = try parseTokensWithStats(std.testing.allocator, token_stream.tokens); + defer parsed.stream.deinit(); + + try std.testing.expectEqual(parsed.stream.events.len, parsed.stats.event_count); + try std.testing.expectEqual(@as(usize, 6), parsed.stats.max_scalar_bytes); + try std.testing.expectEqual(@as(usize, 3), parsed.stats.max_nesting_depth); + try std.testing.expectEqual(@as(usize, 0), parsed.stats.current_nesting_depth); + try std.testing.expect(!parsed.stats.malformed_nesting); +} diff --git a/tests/allocation/failure_injection_test.zig b/tests/allocation/failure_injection_test.zig index 94dacf1d..be365b75 100644 --- a/tests/allocation/failure_injection_test.zig +++ b/tests/allocation/failure_injection_test.zig @@ -74,6 +74,9 @@ fn checkParseEventsAllocationFailure(failing_allocator: std.mem.Allocator) !void \\ ? !e!key "quoted\nkey" \\ : [*root, {plain: value}] \\ + , + \\&root [plain, key: &node ! [*node, {inner: value}], ? explicit : , : empty] + \\ }; for (inputs) |input| { diff --git a/tests/unit/api/parse_test.zig b/tests/unit/api/parse_test.zig index 3cd8ce7a..b87e0eea 100644 --- a/tests/unit/api/parse_test.zig +++ b/tests/unit/api/parse_test.zig @@ -30,6 +30,22 @@ test "parseEvents preserves escaped whitespace before folded double quoted line try std.testing.expectEqualStrings("kept \xc2\xa0 next", stream.events[2].scalar.value); } +test "parseEvents owns flow sequence speculative event strings" { + const input = try std.testing.allocator.dupe(u8, "&root [plain, key: &node ! [*node]]\n"); + defer std.testing.allocator.free(input); + + var stream = try parseEvents(std.testing.allocator, input); + defer stream.deinit(); + @memset(input, 'x'); + + try std.testing.expectEqualStrings("root", stream.events[2].sequence_start.anchor.?); + try std.testing.expectEqualStrings("plain", stream.events[3].scalar.value); + try std.testing.expectEqualStrings("key", stream.events[5].scalar.value); + try std.testing.expectEqualStrings("node", stream.events[6].sequence_start.anchor.?); + try std.testing.expectEqualStrings("tag:example.com,2000:seq", stream.events[6].sequence_start.tag.?); + try std.testing.expectEqualStrings("node", stream.events[7].alias); +} + test "Parser.init reports token and event count limit diagnostics" { const input = \\- one @@ -80,6 +96,30 @@ test "Parser.init reports input scalar and nesting limit diagnostics" { try std.testing.expectEqual(@as(usize, 10), diagnostic.offset); } +test "Parser.init preserves flow sequence implicit mapping limit diagnostics" { + const input = "[plain, key: [nested], tail]\n"; + var diagnostic: Diagnostic = .{}; + try std.testing.expectError(ParseError.Unsupported, yaml.Parser.init(std.testing.allocator, input, .{ + .max_event_count = 13, + .diagnostic = &diagnostic, + })); + try std.testing.expectEqualStrings("event count exceeds configured limit", diagnostic.message); + + diagnostic = .{}; + try std.testing.expectError(ParseError.Unsupported, yaml.Parser.init(std.testing.allocator, input, .{ + .max_scalar_bytes = 5, + .diagnostic = &diagnostic, + })); + try std.testing.expectEqualStrings("scalar exceeds configured size limit", diagnostic.message); + + diagnostic = .{}; + try std.testing.expectError(ParseError.Unsupported, yaml.Parser.init(std.testing.allocator, input, .{ + .max_nesting_depth = 2, + .diagnostic = &diagnostic, + })); + try std.testing.expectEqualStrings("nesting depth exceeds configured limit", diagnostic.message); +} + test "load keeps hash characters inside double quoted block mapping scalars" { var document = try load(std.testing.allocator, \\key: "value # not a comment" diff --git a/tests/unit/parser/flow_test.zig b/tests/unit/parser/flow_test.zig index 1cdd7813..4558bb7b 100644 --- a/tests/unit/parser/flow_test.zig +++ b/tests/unit/parser/flow_test.zig @@ -1098,6 +1098,43 @@ test "parseTokens parses a trailing empty flow mapping value" { try std.testing.expect(event_stream.events[7] == .stream_end); } +test "parseTokens preserves mixed flow sequence event equivalence" { + var token_stream = try scanner.scan(std.testing.allocator, "&root [plain, key: &node ! [*node, {inner: value}], ? explicit : , : empty]\n"); + defer token_stream.deinit(); + + var event_stream = try parseTokens(std.testing.allocator, token_stream.tokens); + defer event_stream.deinit(); + + const expected = [_]types.Event{ + .stream_start, + .{ .document_start = .{} }, + .{ .sequence_start = .{ .style = .flow, .anchor = "root" } }, + .{ .scalar = .{ .value = "plain" } }, + .{ .mapping_start = .{ .style = .flow } }, + .{ .scalar = .{ .value = "key" } }, + .{ .sequence_start = .{ .style = .flow, .anchor = "node", .tag = "tag:example.com,2000:seq" } }, + .{ .alias = "node" }, + .{ .mapping_start = .{ .style = .flow } }, + .{ .scalar = .{ .value = "inner" } }, + .{ .scalar = .{ .value = "value" } }, + .mapping_end, + .sequence_end, + .mapping_end, + .{ .mapping_start = .{ .style = .flow } }, + .{ .scalar = .{ .value = "explicit" } }, + .{ .scalar = .{ .value = "" } }, + .mapping_end, + .{ .mapping_start = .{ .style = .flow } }, + .{ .scalar = .{ .value = "" } }, + .{ .scalar = .{ .value = "empty" } }, + .mapping_end, + .sequence_end, + .{ .document_end = .{} }, + .stream_end, + }; + try std.testing.expectEqualDeep(&expected, event_stream.events); +} + test "parseTokens does not overallocate temporary flow sequence entry events" { const item_count = 96; @@ -1118,7 +1155,7 @@ test "parseTokens does not overallocate temporary flow sequence entry events" { defer event_stream.deinit(); try std.testing.expectEqual(@as(usize, item_count + 6), event_stream.events.len); - try std.testing.expect(counted.allocated_bytes <= input.items.len * 256); + try std.testing.expect(counted.allocated_bytes <= input.items.len * 72); } const CountingAllocator = struct {