Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/parser/flow.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
45 changes: 45 additions & 0 deletions src/parser/internal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -73,13 +88,23 @@ 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);
}

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" {
Expand All @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions src/parser/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
3 changes: 3 additions & 0 deletions tests/allocation/failure_injection_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ fn checkParseEventsAllocationFailure(failing_allocator: std.mem.Allocator) !void
\\ ? !e!key "quoted\nkey"
\\ : [*root, {plain: value}]
\\
,
\\&root [plain, key: &node !<tag:example.com,2000:seq> [*node, {inner: value}], ? explicit : , : empty]
\\
};

for (inputs) |input| {
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/api/parse_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 !<tag:example.com,2000:seq> [*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
Expand Down Expand Up @@ -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"
Expand Down
39 changes: 38 additions & 1 deletion tests/unit/parser/flow_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 !<tag:example.com,2000:seq> [*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;

Expand All @@ -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 {
Expand Down
Loading