|
| 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 | +} |
0 commit comments