source_text.zig (7990B)
1 //! Unicode scalar boundary for source IO and the browser adapter. Binary/string ABI codecs remain byte-oriented. 2 const std = @import("std"); 3 const Arena = @import("arena.zig").Arena; 4 const codecs = @import("codecs.zig"); 5 const reduce = @import("reduce.zig"); 6 7 pub fn fromUtf8(arena: *Arena, bytes: []const u8) !u32 { 8 const view = std.unicode.Utf8View.init(bytes) catch return error.InvalidUtf8; 9 var iter = view.iterator(); 10 var scalars: std.ArrayList(u32) = .empty; 11 defer scalars.deinit(arena.allocator); 12 while (iter.nextCodepoint()) |scalar| { 13 try scalars.append(arena.allocator, try codecs.ofNumber(arena, scalar)); 14 } 15 return codecs.ofList(arena, scalars.items); 16 } 17 18 // Match the Haskell source boundary's canonical-tree check, not merely its 19 // numeric interpretation. Reject redundant high zero bits, non-Boolean bits, 20 // overlong integers, surrogates and values beyond the Unicode scalar range. 21 fn scalarNumber(arena: *Arena, input: u32, unbounded: bool) !u21 { 22 var current = input; 23 var value: u32 = 0; 24 var bits: u5 = 0; 25 var high_bit: u32 = 0; 26 while (true) { 27 const node = arena.get(try observe(arena, current, unbounded)).*; 28 switch (node) { 29 .leaf => { 30 if (bits != 0 and high_bit == 0) return error.InvalidScalarList; 31 if (value > 0x10ffff or (value >= 0xd800 and value <= 0xdfff)) return error.InvalidScalarList; 32 return @intCast(value); 33 }, 34 .fork => |pair| { 35 if (bits == 21) return error.InvalidScalarList; 36 const bit = arena.get(try observe(arena, pair.left, unbounded)).*; 37 high_bit = switch (bit) { 38 .leaf => 0, 39 .stem => |s| if (arena.get(try observe(arena, s.child, unbounded)).* == .leaf) 1 else return error.InvalidScalarList, 40 else => return error.InvalidScalarList, 41 }; 42 value |= high_bit << bits; 43 bits += 1; 44 current = pair.right; 45 }, 46 else => return error.InvalidScalarList, 47 } 48 } 49 } 50 51 pub fn toUtf8(arena: *Arena, input: u32) ![]u8 { 52 return toUtf8Bounded(arena, input, std.math.maxInt(usize)); 53 } 54 55 // A scalar-count ceiling, not a fuel/deadline limit. Do not demand elements 56 // beyond the ceiling; still inspect the spine to distinguish exact length. 57 fn observe(arena: *Arena, input: u32, unbounded: bool) !u32 { 58 return if (unbounded) reduce.reduceUnbounded(input, arena) else reduce.reduce(input, arena, 1_000_000); 59 } 60 61 pub fn toUtf8UnboundedWork(arena: *Arena, input: u32, limit: usize) ![]u8 { 62 return decode(arena, input, limit, true); 63 } 64 65 pub fn toUtf8Bounded(arena: *Arena, input: u32, limit: usize) ![]u8 { 66 return decode(arena, input, limit, false); 67 } 68 69 fn decode(arena: *Arena, input: u32, limit: usize, unbounded: bool) ![]u8 { 70 const base = arena.roots.items.len; 71 defer arena.roots.shrinkRetainingCapacity(base); 72 try arena.roots.append(arena.allocator, input); 73 var bytes: std.ArrayList(u8) = .empty; 74 errdefer bytes.deinit(arena.allocator); 75 var current = input; 76 var count: usize = 0; 77 while (true) { 78 const node = arena.get(try observe(arena, current, unbounded)).*; 79 switch (node) { 80 .leaf => return bytes.toOwnedSlice(arena.allocator), 81 .fork => |pair| { 82 if (count == limit) return error.ScalarLimit; 83 count += 1; 84 const scalar = try scalarNumber(arena, pair.left, unbounded); 85 var encoded: [4]u8 = undefined; 86 const len = try std.unicode.utf8Encode(scalar, &encoded); 87 try bytes.appendSlice(arena.allocator, encoded[0..len]); 88 current = pair.right; 89 }, 90 else => return error.InvalidScalarList, 91 } 92 } 93 } 94 95 test "source scalar codec preserves exact UTF-8 including NUL, CRLF and noncharacters" { 96 var arena = Arena.init(std.testing.allocator); 97 defer arena.deinit(); 98 const bytes = "\x00a\r\nĆ©š\u{ffff}\u{10ffff}\u{feff}"; 99 const scalars = try fromUtf8(&arena, bytes); 100 var list = (try codecs.toList(&arena, scalars)).?; 101 defer list.deinit(arena.allocator); 102 const expected = [_]u64{ 0, 0x61, 13, 10, 0xe9, 0x1f600, 0xffff, 0x10ffff, 0xfeff }; 103 try std.testing.expectEqual(expected.len, list.items.len); 104 for (list.items, expected) |item, n| try std.testing.expectEqual(n, (try codecs.toNumber(&arena, item)).?); 105 arena.gc_enabled = true; 106 try arena.setGcPolicy(1, 1); 107 const result = try toUtf8(&arena, scalars); 108 defer arena.allocator.free(result); 109 try std.testing.expectEqualStrings(bytes, result); 110 try std.testing.expect(arena.collections > 0); 111 try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len); 112 } 113 114 test "source scalar codec rejects malformed UTF-8 and scalar trees" { 115 var arena = Arena.init(std.testing.allocator); 116 defer arena.deinit(); 117 for ([_][]const u8{ "\x80", "\xc0\xaf", "\xe2\x82", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xff" }) |bytes| { 118 try std.testing.expectError(error.InvalidUtf8, fromUtf8(&arena, bytes)); 119 } 120 for ([_]u64{ 0xd800, 0xdfff, 0x110000, std.math.maxInt(u64) }) |n| { 121 const list = try codecs.ofList(&arena, &.{try codecs.ofNumber(&arena, n)}); 122 try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, list)); 123 } 124 const leaf = try arena.alloc(.leaf); 125 const zero_with_bit = try arena.alloc(.{ .fork = .{ .left = leaf, .right = leaf } }); 126 const bad_bit = try arena.alloc(.{ .stem = .{ .child = zero_with_bit } }); 127 const malformed = try arena.alloc(.{ .fork = .{ .left = bad_bit, .right = leaf } }); 128 for ([_]u32{ zero_with_bit, malformed, bad_bit }) |n| { 129 const list = try codecs.ofList(&arena, &.{n}); 130 try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, list)); 131 } 132 try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, bad_bit)); 133 // Failure after successful UTF-8 output must free the partial buffer too. 134 const partial = try codecs.ofList(&arena, &.{ try codecs.ofNumber(&arena, 65), try codecs.ofNumber(&arena, 0xd800) }); 135 try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, partial)); 136 } 137 138 test "source scalar codec accepts lazy scalar numerals" { 139 var arena = Arena.init(std.testing.allocator); 140 defer arena.deinit(); 141 const leaf = try arena.alloc(.leaf); 142 const scalar = try codecs.ofNumber(&arena, 0x1f600); 143 const constant = try arena.alloc(.{ .fork = .{ .left = leaf, .right = scalar } }); 144 const delayed = try arena.alloc(.{ .app = .{ .func = constant, .arg = leaf } }); 145 const list = try codecs.ofList(&arena, &.{delayed}); 146 arena.gc_enabled = true; 147 try arena.setGcPolicy(1, 1); 148 const bytes = try toUtf8(&arena, list); 149 defer arena.allocator.free(bytes); 150 try std.testing.expectEqualStrings("š", bytes); 151 } 152 153 fn allocationProbe(allocator: std.mem.Allocator) !void { 154 var arena = Arena.init(allocator); 155 defer arena.deinit(); 156 const scalars = try fromUtf8(&arena, "aĆ©š"); 157 const bytes = try toUtf8(&arena, scalars); 158 defer allocator.free(bytes); 159 try std.testing.expectEqualStrings("aĆ©š", bytes); 160 } 161 162 test "source scalar codec cleans up on every allocation failure" { 163 try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationProbe, .{}); 164 } 165 166 167 test "bounded scalar encoding handles exact limits and unwinds partial output" { 168 var arena = Arena.init(std.testing.allocator); 169 defer arena.deinit(); 170 const input = try fromUtf8(&arena, "Ć©š"); 171 const bytes = try toUtf8Bounded(&arena, input, 2); 172 defer arena.allocator.free(bytes); 173 try std.testing.expectEqualStrings("Ć©š", bytes); 174 try std.testing.expectError(error.ScalarLimit, toUtf8Bounded(&arena, input, 1)); 175 try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len); 176 }