lib/pretty/core/src/json/escape.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub fn writeString(writer: anytype, value: []const u8) !void {
4 try std.json.Stringify.value(value, .{}, writer);
5 }
6
7 pub fn writeStringParts(writer: anytype, parts: []const []const u8) !void {
8 for (parts) |part| {
9 if (!std.unicode.utf8ValidateSlice(part)) return error.InvalidUtf8;
10 }
11 try writer.writeByte('"');
12 for (parts) |part| try writeStringContent(writer, part);
13 try writer.writeByte('"');
14 }
15
16 pub fn writeByteString(writer: anytype, value: []const u8) !void {
17 const hex = "0123456789abcdef";
18 try writer.writeByte('"');
19 for (value) |byte| {
20 switch (byte) {
21 '"' => try writer.writeAll("\\\""),
22 '\\' => try writer.writeAll("\\\\"),
23 '\x08' => try writer.writeAll("\\b"),
24 '\x0c' => try writer.writeAll("\\f"),
25 '\n' => try writer.writeAll("\\n"),
26 '\r' => try writer.writeAll("\\r"),
27 '\t' => try writer.writeAll("\\t"),
28 0x20...0x21, 0x23...0x5b, 0x5d...0x7e => try writer.writeByte(byte),
29 else => try writer.writeAll(&[_]u8{
30 '\\',
31 'u',
32 '0',
33 '0',
34 hex[byte >> 4],
35 hex[byte & 0x0f],
36 }),
37 }
38 }
39 try writer.writeByte('"');
40 }
41
42 pub fn stringAlloc(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
43 return try std.json.Stringify.valueAlloc(allocator, value, .{});
44 }
45
46 fn writeStringContent(writer: anytype, value: []const u8) !void {
47 for (value) |byte| {
48 switch (byte) {
49 '"' => try writer.writeAll("\\\""),
50 '\\' => try writer.writeAll("\\\\"),
51 '\n' => try writer.writeAll("\\n"),
52 '\r' => try writer.writeAll("\\r"),
53 '\t' => try writer.writeAll("\\t"),
54 else => {
55 if (byte < 0x20) {
56 try writer.print("\\u{x:0>4}", .{byte});
57 } else {
58 try writer.writeByte(byte);
59 }
60 },
61 }
62 }
63 }
64
65 test "writeString escapes JSON string content" {
66 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
67 defer out.deinit();
68 try writeString(&out.writer, "a\n\"b\"");
69 const rendered = try out.toOwnedSlice();
70 defer std.testing.allocator.free(rendered);
71 try std.testing.expectEqualStrings("\"a\\n\\\"b\\\"\"", rendered);
72 }
73
74 test "writeStringParts escapes composed JSON string content" {
75 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
76 defer out.deinit();
77 try writeStringParts(&out.writer, &.{ "forum:", "post\"id", "#", "reply\nid" });
78 const rendered = try out.toOwnedSlice();
79 defer std.testing.allocator.free(rendered);
80 try std.testing.expectEqualStrings("\"forum:post\\\"id#reply\\nid\"", rendered);
81 }
82
83 test "writeStringParts rejects invalid utf8 fragments" {
84 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
85 defer out.deinit();
86 try std.testing.expectError(error.InvalidUtf8, writeStringParts(&out.writer, &.{"\xff"}));
87 try std.testing.expectEqualStrings("", out.written());
88 }
89
90 test "writeByteString escapes arbitrary bytes as lowercase code points" {
91 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
92 defer out.deinit();
93 try writeByteString(&out.writer, "quote\"\n\x01\x7f\x80\xff");
94 try std.testing.expectEqualStrings(
95 "\"quote\\\"\\n\\u0001\\u007f\\u0080\\u00ff\"",
96 out.written(),
97 );
98 }