summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeffrey C. Ollie <jeff@ocjtech.us>2025-10-10 12:00:50 -0500
committerGitHub <noreply@github.com>2025-10-10 10:00:50 -0700
commit7767a4577989b788baa8ebe74b9e6c9cd37e5cbb (patch)
tree11d257f32173bc85af5e049858455972e119d120
parentbac2419343e921f57ff56a6f2cd5e5d9ded8d6c2 (diff)
osc: do inplace decoding of cmdline passed in OSC 133;C (#9127)
-rw-r--r--src/os/string_encoding.zig267
-rw-r--r--src/terminal/osc.zig255
2 files changed, 504 insertions, 18 deletions
diff --git a/src/os/string_encoding.zig b/src/os/string_encoding.zig
new file mode 100644
index 000000000..162023ad2
--- /dev/null
+++ b/src/os/string_encoding.zig
@@ -0,0 +1,267 @@
+const std = @import("std");
+
+/// Do an in-place decode of a string that has been encoded in the same way
+/// that `bash`'s `printf %q` encodes a string. This is safe because a string
+/// can only get shorter after decoding. This destructively modifies the buffer
+/// given to it. If an error is returned the buffer may be in an unusable state.
+pub fn printfQDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 {
+ const data: [:0]u8 = data: {
+ // Strip off `$''` quoting.
+ if (std.mem.startsWith(u8, buf, "$'")) {
+ if (buf.len < 3 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError;
+ buf[buf.len - 1] = 0;
+ break :data buf[2 .. buf.len - 1 :0];
+ }
+ // Strip off `''` quoting.
+ if (std.mem.startsWith(u8, buf, "'")) {
+ if (buf.len < 2 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError;
+ buf[buf.len - 1] = 0;
+ break :data buf[1 .. buf.len - 1 :0];
+ }
+ break :data buf;
+ };
+
+ var src: usize = 0;
+ var dst: usize = 0;
+
+ while (src < data.len) {
+ switch (data[src]) {
+ else => {
+ data[dst] = data[src];
+ src += 1;
+ dst += 1;
+ },
+ '\\' => {
+ if (src + 1 >= data.len) return error.DecodeError;
+ switch (data[src + 1]) {
+ ' ',
+ '\\',
+ '"',
+ '\'',
+ '$',
+ => |c| {
+ data[dst] = c;
+ src += 2;
+ dst += 1;
+ },
+ 'e' => {
+ data[dst] = std.ascii.control_code.esc;
+ src += 2;
+ dst += 1;
+ },
+ 'n' => {
+ data[dst] = std.ascii.control_code.lf;
+ src += 2;
+ dst += 1;
+ },
+ 'r' => {
+ data[dst] = std.ascii.control_code.cr;
+ src += 2;
+ dst += 1;
+ },
+ 't' => {
+ data[dst] = std.ascii.control_code.ht;
+ src += 2;
+ dst += 1;
+ },
+ 'v' => {
+ data[dst] = std.ascii.control_code.vt;
+ src += 2;
+ dst += 1;
+ },
+ else => return error.DecodeError,
+ }
+ },
+ }
+ }
+
+ data[dst] = 0;
+ return data[0..dst :0];
+}
+
+test "printf_q 1" {
+ const s: [:0]const u8 = "bobr\\ kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ const dst = try printfQDecode(&src);
+ try std.testing.expectEqualStrings("bobr kurwa", dst);
+}
+
+test "printf_q 2" {
+ const s: [:0]const u8 = "bobr\\nkurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ const dst = try printfQDecode(&src);
+ try std.testing.expectEqualStrings("bobr\nkurwa", dst);
+}
+
+test "printf_q 3" {
+ const s: [:0]const u8 = "bobr\\dkurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&src));
+}
+
+test "printf_q 4" {
+ const s: [:0]const u8 = "bobr kurwa\\";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&src));
+}
+
+test "printf_q 5" {
+ const s: [:0]const u8 = "$'bobr kurwa'";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ const dst = try printfQDecode(&src);
+ try std.testing.expectEqualStrings("bobr kurwa", dst);
+}
+
+test "printf_q 6" {
+ const s: [:0]const u8 = "'bobr kurwa'";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ const dst = try printfQDecode(&src);
+ try std.testing.expectEqualStrings("bobr kurwa", dst);
+}
+
+test "printf_q 7" {
+ const s: [:0]const u8 = "$'bobr kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&src));
+}
+
+test "printf_q 8" {
+ const s: [:0]const u8 = "$'";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&src));
+}
+
+test "printf_q 9" {
+ const s: [:0]const u8 = "'bobr kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&src));
+}
+
+test "printf_q 10" {
+ const s: [:0]const u8 = "'";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&src));
+}
+
+/// Do an in-place decode of a string that has been URL percent encoded.
+/// This is safe because a string can only get shorter after decoding. This
+/// destructively modifies the buffer given to it. If an error is returned the
+/// buffer may be in an unusable state.
+pub fn urlPercentDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 {
+ var src: usize = 0;
+ var dst: usize = 0;
+ while (src < buf.len) {
+ switch (buf[src]) {
+ else => {
+ buf[dst] = buf[src];
+ src += 1;
+ dst += 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' => {
+ buf[dst] = std.math.shl(u8, hex(buf[src + 1]), 4) | hex(buf[src + 2]);
+ src += 3;
+ dst += 1;
+ },
+ else => return error.DecodeError,
+ }
+ },
+ else => return error.DecodeError,
+ }
+ },
+ }
+ }
+ buf[dst] = 0;
+ return buf[0..dst :0];
+}
+
+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 buf_: [4]u8 = undefined;
+ const buf = try std.fmt.bufPrintZ(&buf_, "%{x:0>2}", .{c});
+ const decoded = try urlPercentDecode(buf);
+ try std.testing.expectEqual(1, decoded.len);
+ try std.testing.expectEqual(c, decoded[0]);
+ }
+ for (0..255) |c| {
+ var buf_: [4]u8 = undefined;
+ const buf = try std.fmt.bufPrintZ(&buf_, "%{X:0>2}", .{c});
+ const decoded = try urlPercentDecode(buf);
+ try std.testing.expectEqual(1, decoded.len);
+ try std.testing.expectEqual(c, decoded[0]);
+ }
+}
+
+test "percent 1" {
+ const s: [:0]const u8 = "bobr%20kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ const dst = try urlPercentDecode(&src);
+ try std.testing.expectEqualStrings("bobr kurwa", dst);
+}
+
+test "percent 2" {
+ const s: [:0]const u8 = "bobr%2kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&src));
+}
+
+test "percent 3" {
+ const s: [:0]const u8 = "bobr%kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&src));
+}
+
+test "percent 4" {
+ const s: [:0]const u8 = "bobr%%kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&src));
+}
+
+test "percent 5" {
+ const s: [:0]const u8 = "bobr%20kurwa%20";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ const dst = try urlPercentDecode(&src);
+ try std.testing.expectEqualStrings("bobr kurwa ", dst);
+}
+
+test "percent 6" {
+ const s: [:0]const u8 = "bobr%20kurwa%2";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&src));
+}
+
+test "percent 7" {
+ const s: [:0]const u8 = "bobr%20kurwa%";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&src));
+}
diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig
index 4b0f9553c..12a6d1f5c 100644
--- a/src/terminal/osc.zig
+++ b/src/terminal/osc.zig
@@ -15,6 +15,7 @@ const LibEnum = @import("../lib/enum.zig").Enum;
const RGB = @import("color.zig").RGB;
const kitty_color = @import("kitty/color.zig");
const osc_color = @import("osc/color.zig");
+const string_encoding = @import("../os/string_encoding.zig");
pub const color = osc_color;
const log = std.log.scoped(.osc);
@@ -89,12 +90,7 @@ pub const Command = union(Key) {
end_of_input: struct {
/// The command line that the user entered.
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
- cmdline: ?union(enum) {
- /// The command line has been encoded with bash's 'printf "%q"'.
- printf_q_encoded: [:0]const u8,
- /// The command line has been encoded with URL percent encoding.
- percent_encoded: [:0]const u8,
- } = null,
+ cmdline: ?[:0]const u8 = null,
},
/// End of current command.
@@ -1482,17 +1478,13 @@ pub const Parser = struct {
} else if (mem.eql(u8, self.temp_state.key, "cmdline")) {
// https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
switch (self.command) {
- .end_of_input => |*v| v.cmdline = .{
- .printf_q_encoded = value,
- },
+ .end_of_input => |*v| v.cmdline = string_encoding.printfQDecode(value) catch null,
else => {},
}
} else if (mem.eql(u8, self.temp_state.key, "cmdline_url")) {
// https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
switch (self.command) {
- .end_of_input => |*v| v.cmdline = .{
- .percent_encoded = value,
- },
+ .end_of_input => |*v| v.cmdline = string_encoding.urlPercentDecode(value) catch null,
else => {},
}
} else if (mem.eql(u8, self.temp_state.key, "redraw")) {
@@ -3063,7 +3055,7 @@ test "OSC 133: end_of_input" {
try testing.expect(cmd == .end_of_input);
}
-test "OSC 133: end_of_input with cmdline" {
+test "OSC 133: end_of_input with cmdline 1" {
const testing = std.testing;
var p: Parser = .init();
@@ -3074,11 +3066,132 @@ test "OSC 133: end_of_input with cmdline" {
const cmd = p.end(null).?.*;
try testing.expect(cmd == .end_of_input);
try testing.expect(cmd.end_of_input.cmdline != null);
- try testing.expect(cmd.end_of_input.cmdline.? == .printf_q_encoded);
- try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?.printf_q_encoded);
+ try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=echo bobr\\ kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline 3" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=echo bobr\\nkurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr\nkurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline 4" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=$'echo bobr kurwa'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline 5" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline='echo bobr kurwa'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline 6" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline='echo bobr kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline 7" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=$'echo bobr kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline 8" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=$'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline 9" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=$'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline 10" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline=";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("", cmd.end_of_input.cmdline.?);
}
-test "OSC 133: end_of_input with cmdline_url" {
+test "OSC 133: end_of_input with cmdline_url 1" {
const testing = std.testing;
var p: Parser = .init();
@@ -3089,8 +3202,114 @@ test "OSC 133: end_of_input with cmdline_url" {
const cmd = p.end(null).?.*;
try testing.expect(cmd == .end_of_input);
try testing.expect(cmd.end_of_input.cmdline != null);
- try testing.expect(cmd.end_of_input.cmdline.? == .percent_encoded);
- try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?.percent_encoded);
+ try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline_url 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr%20kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline_url 3" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr%3bkurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr;kurwa", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline_url 4" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr%3kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline_url 5" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr%kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline_url 6" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr%kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline_url 7" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr kurwa%20";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline != null);
+ try testing.expectEqualStrings("echo bobr kurwa ", cmd.end_of_input.cmdline.?);
+}
+
+test "OSC 133: end_of_input with cmdline_url 8" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr kurwa%2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
+}
+
+test "OSC 133: end_of_input with cmdline_url 9" {
+ const testing = std.testing;
+
+ var p: Parser = .init();
+
+ const input = "133;C;cmdline_url=echo bobr kurwa%2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .end_of_input);
+ try testing.expect(cmd.end_of_input.cmdline == null);
}
test "OSC: OSC 777 show desktop notification with title" {