diff options
| author | sl0p <sl0p@sl0p.foo> | 2026-08-13 22:00:03 +0200 |
|---|---|---|
| committer | sl0p <sl0p@sl0p.foo> | 2026-08-13 22:00:03 +0200 |
| commit | 2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8 (patch) | |
| tree | 7cd6274303ba1da1f8bd57371aec336dd687fb58 /vendor/libghostty-vt/src/os/string_encoding.zig | |
| download | slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.tar.gz slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.tar.xz slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.zip | |
sl0ptty: M0 — libghostty-vt vendored, one pane, compositor, diff renderer
Design contract in DESIGN.md. libghostty-vt vendored fresh from ghostty
main@bedb6f2 (zig 0.16), built to a 14MB static lib.
M0: pty spawn (no libutil), a lib-vt terminal per pane, a screen cell buffer,
a diff emitter that repaints only what changed, raw mode, SIGWINCH, and a
~120Hz frame cap that coalesces pty reads.
Also the seed of the headless harness: --headless dumps the composited screen
without a tty, so composition is testable in milliseconds.
tests/live_m0.py covers what headless cannot: raw mode, the diff emitter,
resize propagation, and the quit path. 6/6 green.
Diffstat (limited to 'vendor/libghostty-vt/src/os/string_encoding.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/os/string_encoding.zig | 307 |
1 files changed, 307 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/os/string_encoding.zig b/vendor/libghostty-vt/src/os/string_encoding.zig new file mode 100644 index 0000000..875a5bc --- /dev/null +++ b/vendor/libghostty-vt/src/os/string_encoding.zig @@ -0,0 +1,307 @@ +const std = @import("std"); + +/// Decode date from the buffer that has been encoded in the same way that +/// `bash`'s `printf %q` encodes a string and write it to the writer. If an +/// error is returned garbage may have been written to the buffer. +pub fn printfQDecode(writer: *std.Io.Writer, buf: []const u8) (std.Io.Writer.Error || error{DecodeError})!void { + const data: []const u8 = data: { + // Strip off `$''` quoting. + if (std.mem.startsWith(u8, buf, "$'")) { + if (buf.len < 3 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError; + break :data buf[2 .. buf.len - 1]; + } + // Strip off `''` quoting. + if (std.mem.startsWith(u8, buf, "'")) { + if (buf.len < 2 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError; + break :data buf[1 .. buf.len - 1]; + } + break :data buf; + }; + + var src: usize = 0; + + while (src < data.len) { + switch (data[src]) { + else => { + try writer.writeByte(data[src]); + src += 1; + }, + '\\' => { + if (src + 1 >= data.len) return error.DecodeError; + switch (data[src + 1]) { + ' ', + '\\', + '"', + '\'', + '$', + => |c| { + try writer.writeByte(c); + src += 2; + }, + 'e' => { + try writer.writeByte(std.ascii.control_code.esc); + src += 2; + }, + 'n' => { + try writer.writeByte(std.ascii.control_code.lf); + src += 2; + }, + 'r' => { + try writer.writeByte(std.ascii.control_code.cr); + src += 2; + }, + 't' => { + try writer.writeByte(std.ascii.control_code.ht); + src += 2; + }, + 'v' => { + try writer.writeByte(std.ascii.control_code.vt); + src += 2; + }, + else => return error.DecodeError, + } + }, + } + } +} + +test "printf_q 1" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr\\ kurwa"; + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); +} + +test "printf_q 2" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "bobr\\nkurwa"; + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr\nkurwa", w.written()); +} + +test "printf_q 3" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "bobr\\dkurwa"; + + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); +} + +test "printf_q 4" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "bobr kurwa\\"; + + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); +} + +test "printf_q 5" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "$'bobr kurwa'"; + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); +} + +test "printf_q 6" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "'bobr kurwa'"; + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); +} + +test "printf_q 7" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "$'bobr kurwa"; + + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); +} + +test "printf_q 8" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + const s: [:0]const u8 = "$'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); +} + +test "printf_q 9" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + const s: [:0]const u8 = "'bobr kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); +} + +test "printf_q 10" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: [:0]const u8 = "'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); +} + +/// Decode data from the buffer that has been URL percent encoded and write +/// it to the given buffer. If an error is returned the garbage may have been +/// written to the writer. +pub fn urlPercentDecode(writer: *std.Io.Writer, buf: []const u8) (std.Io.Writer.Error || error{DecodeError})!void { + var src: usize = 0; + while (src < buf.len) { + switch (buf[src]) { + else => { + try writer.writeByte(buf[src]); + src += 1; + }, + '%' => { + if (src + 2 >= buf.len) return error.DecodeError; + switch (buf[src + 1]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + switch (buf[src + 2]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + try writer.writeByte(std.math.shl(u8, hex(buf[src + 1]), 4) | hex(buf[src + 2])); + src += 3; + }, + else => return error.DecodeError, + } + }, + else => return error.DecodeError, + } + }, + } + } +} + +inline fn hex(c: u8) u4 { + switch (c) { + '0'...'9' => return @truncate(c - '0'), + 'a'...'f' => return @truncate(c - 'a' + 10), + 'A'...'F' => return @truncate(c - 'A' + 10), + else => unreachable, + } +} + +test "singles percent" { + for (0..255) |c| { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + var buf_: [4]u8 = undefined; + const buf = try std.fmt.bufPrintZ(&buf_, "%{x:0>2}", .{c}); + + try urlPercentDecode(&w.writer, buf); + const decoded = w.written(); + + try std.testing.expectEqual(1, decoded.len); + try std.testing.expectEqual(c, decoded[0]); + } + for (0..255) |c| { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + var buf_: [4]u8 = undefined; + const buf = try std.fmt.bufPrintZ(&buf_, "%{X:0>2}", .{c}); + + try urlPercentDecode(&w.writer, buf); + const decoded = w.written(); + + try std.testing.expectEqual(1, decoded.len); + try std.testing.expectEqual(c, decoded[0]); + } +} + +test "percent 1" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa"; + + try urlPercentDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); +} + +test "percent 2" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%2kurwa"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); +} + +test "percent 3" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%kurwa"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); +} + +test "percent 4" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%%kurwa"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); +} + +test "percent 5" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa%20"; + + try urlPercentDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa ", w.written()); +} + +test "percent 6" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa%2"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); +} + +test "percent 7" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa%"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); +} + +/// Is the given character valid in URI percent encoding? +fn isValidChar(c: u8) bool { + return switch (c) { + ' ', ';', '=' => false, + else => return std.ascii.isPrint(c), + }; +} + +/// Write data to the writer after URI percent encoding. +pub fn urlPercentEncode(writer: *std.Io.Writer, data: []const u8) std.Io.Writer.Error!void { + try std.Uri.Component.percentEncode(writer, data, isValidChar); +} |
