Skip to content

Commit 9bef40b

Browse files
author
Pascal Post
committed
path encoding added
1 parent a0d5bfe commit 9bef40b

File tree

6 files changed

+112
-7
lines changed

6 files changed

+112
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The validator currently uses C++ regex implementation for pattern matching, requ
2828
- [x] const.json
2929
- [ ] contains.json
3030
- [ ] default.json
31-
- [ ] definitions.jsoe
31+
- [ ] definitions.json
3232
- [ ] dependencies.json
3333
- [x] enum.json
3434
- [x] exclusiveMaximum.json

src/generic.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ pub fn checks(node: std.json.ObjectMap, data: std.json.Value, stack: *Stack, err
99
if (node.get("$ref")) |ref| {
1010
std.debug.assert(ref == .string);
1111
const ref_path = ref.string;
12-
const node_ref = (try stack.value(ref_path)).?;
12+
const node_ref = (try stack.value(ref_path)) orelse {
13+
std.debug.print("ref_path could not be found: {s}\n", .{ref_path});
14+
unreachable;
15+
};
1316

1417
try stack.pushPath("$ref");
1518
try checkNode(node_ref, data, stack, errors);

src/json_pointer.zig

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const std = @import("std");
2+
3+
pub const Error = error{
4+
InvalidPointerSyntax,
5+
};
6+
7+
/// decode escaped characters in given path (see https://datatracker.ietf.org/doc/html/rfc6901#section-4)
8+
pub const PathDecoderUnmanaged = struct {
9+
buffer: std.ArrayListUnmanaged(u8),
10+
11+
pub fn initCapacity(allocator: std.mem.Allocator, path_len_capacity: usize) !PathDecoderUnmanaged {
12+
return .{
13+
.buffer = try std.ArrayListUnmanaged(u8).initCapacity(allocator, path_len_capacity),
14+
};
15+
}
16+
17+
pub fn deinit(self: *PathDecoderUnmanaged, allocator: std.mem.Allocator) void {
18+
self.buffer.deinit(allocator);
19+
}
20+
21+
/// decode ~0 to ~ and ~1 to /; if not present self.buffer.items.len == 0
22+
fn decodeSlashTilde(self: *PathDecoderUnmanaged, allocator: std.mem.Allocator, path: []const u8) !void {
23+
var head: usize = 0;
24+
while (std.mem.indexOfScalarPos(u8, path, head, '~')) |idx| {
25+
if (idx >= path.len - 1 or (path[idx + 1] != '0' and path[idx + 1] != '1')) {
26+
// error condition for a JSON Pointer
27+
// https://datatracker.ietf.org/doc/html/rfc6901#section-3
28+
return Error.InvalidPointerSyntax;
29+
}
30+
31+
try self.buffer.appendSlice(allocator, path[head..idx]);
32+
33+
const c: u8 = if (path[idx + 1] == '0') '~' else '/';
34+
try self.buffer.append(allocator, c);
35+
head = idx + 2;
36+
}
37+
38+
try self.buffer.appendSlice(allocator, path[head..]);
39+
}
40+
41+
fn percentDecoding(self: *PathDecoderUnmanaged, allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
42+
if (self.buffer.items.len > 0) {
43+
return std.Uri.percentDecodeInPlace(self.buffer.items);
44+
}
45+
46+
var needs_decoding = false;
47+
var input_index = path.len;
48+
while (input_index > 0) {
49+
if (input_index >= 3) {
50+
const maybe_percent_encoded = path[input_index - 3 ..][0..3];
51+
if (maybe_percent_encoded[0] == '%') {
52+
if (std.fmt.parseInt(u8, maybe_percent_encoded[1..], 16)) |_| {
53+
needs_decoding = true;
54+
break;
55+
} else |_| {}
56+
}
57+
}
58+
input_index -= 1;
59+
}
60+
61+
if (needs_decoding) {
62+
try self.buffer.appendSlice(allocator, path);
63+
// NOTE can be made more efficient by not searching the whole path again
64+
// and start with the input_index from above
65+
return std.Uri.percentDecodeInPlace(self.buffer.items);
66+
}
67+
68+
return path;
69+
}
70+
71+
pub fn decode(self: *PathDecoderUnmanaged, allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
72+
self.buffer.clearRetainingCapacity();
73+
try self.decodeSlashTilde(allocator, path);
74+
return try self.percentDecoding(allocator, path);
75+
}
76+
};
77+
78+
test "path decoding" {
79+
const allocator = std.testing.allocator;
80+
var decoder = try PathDecoderUnmanaged.initCapacity(allocator, 1000);
81+
defer decoder.deinit(allocator);
82+
83+
try std.testing.expectError(Error.InvalidPointerSyntax, decoder.decode(allocator, "~"));
84+
try std.testing.expectError(Error.InvalidPointerSyntax, decoder.decode(allocator, "invalid~"));
85+
try std.testing.expectError(Error.InvalidPointerSyntax, decoder.decode(allocator, "invalid~path"));
86+
try std.testing.expectError(Error.InvalidPointerSyntax, decoder.decode(allocator, "~3"));
87+
88+
try std.testing.expectEqualStrings("tilde~field", try decoder.decode(allocator, "tilde~0field"));
89+
try std.testing.expectEqualStrings("slash/field", try decoder.decode(allocator, "slash~1field"));
90+
try std.testing.expectEqualStrings("percent%field", try decoder.decode(allocator, "percent%25field"));
91+
try std.testing.expectEqualStrings("slash/percent%field", try decoder.decode(allocator, "slash~1percent%25field"));
92+
}

src/schema.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ const numeric = @import("numeric.zig");
66
const object = @import("object.zig");
77
const string = @import("string.zig");
88
const array = @import("array.zig");
9+
const json_pointer = @import("json_pointer.zig");
910

1011
const testing = std.testing;
1112

12-
const ErrorSet = error{} || std.mem.Allocator.Error || std.fmt.ParseIntError;
13+
const ErrorSet = std.mem.Allocator.Error || std.fmt.ParseIntError || json_pointer.Error;
1314

1415
pub fn checkSchemaObject(schema: std.json.ObjectMap, data: std.json.Value, stack: *Stack, errors: *Errors) ErrorSet!void {
1516
try generic.checks(schema, data, stack, errors);

src/stack.zig

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const json_pointer = @import("json_pointer.zig");
23
const eql = @import("value.zig").eql;
34

45
const Tag = enum { path_len, index };
@@ -14,20 +15,24 @@ pub const Stack = struct {
1415
path_buffer: std.ArrayListUnmanaged(u8),
1516
data: std.ArrayListUnmanaged(Storage),
1617
root: std.json.Value,
18+
decoder: json_pointer.PathDecoderUnmanaged,
1719

1820
pub fn init(allocator: std.mem.Allocator, root_node: std.json.Value, capacity: usize) !Stack {
1921
// TODO allow to give an estimated depth and init with capacity
22+
const path_capacity = 1000;
2023
return .{
2124
.allocator = allocator,
2225
.path_buffer = try std.ArrayListUnmanaged(u8).initCapacity(allocator, capacity),
2326
.data = try std.ArrayListUnmanaged(Storage).initCapacity(allocator, capacity),
2427
.root = root_node,
28+
.decoder = try json_pointer.PathDecoderUnmanaged.initCapacity(allocator, path_capacity),
2529
};
2630
}
2731

2832
pub fn deinit(self: *Stack) void {
2933
self.path_buffer.deinit(self.allocator);
3034
self.data.deinit(self.allocator);
35+
self.decoder.deinit(self.allocator);
3136
}
3237

3338
pub fn pushPath(self: *Stack, path: []const u8) !void {
@@ -73,24 +78,27 @@ pub const Stack = struct {
7378
return try path.toOwnedSlice();
7479
}
7580

76-
pub fn value(self: Stack, abs_path: []const u8) !?std.json.Value {
81+
pub fn value(self: *Stack, abs_path: []const u8) !?std.json.Value {
82+
std.debug.assert(abs_path.len > 0);
7783
std.debug.assert(abs_path[0] == '#');
7884
if (abs_path.len == 1) return self.root;
7985

8086
var it = try std.fs.path.componentIterator(abs_path[1..]);
8187
var parent = self.root;
8288
while (it.next()) |component| {
89+
const component_path = try self.decoder.decode(self.allocator, component.name);
90+
8391
switch (parent) {
8492
.object => |object| {
85-
if (object.get(component.name)) |v| {
93+
if (object.get(component_path)) |v| {
8694
parent = v;
8795
} else {
88-
std.debug.print("could not find key {s} in object at path {s}\n", .{ component.name, component.path });
96+
std.debug.print("could not find key {s} in object at path {s}\n", .{ component_path, component.path });
8997
return null;
9098
}
9199
},
92100
.array => |array| {
93-
const idx = try std.fmt.parseInt(usize, component.name, 10);
101+
const idx = try std.fmt.parseInt(usize, component_path, 10);
94102
if (idx >= array.items.len) {
95103
std.debug.print("requested array index {} does not exist in array at path {s}\n", .{ idx, component.path });
96104
return null;

src/tests.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ test "all" {
33
_ = @import("stack.zig");
44
_ = @import("regex.zig");
55
_ = @import("test_suite.zig");
6+
_ = @import("json_pointer.zig");
67
}

0 commit comments

Comments
 (0)