diff --git a/example/c-vt-paste/README.md b/example/c-vt-paste/README.md index 0f911771f..377cd3c3b 100644 --- a/example/c-vt-paste/README.md +++ b/example/c-vt-paste/README.md @@ -1,7 +1,7 @@ -# Example: `ghostty-vt` Paste Safety Check +# Example: `ghostty-vt` Paste Utilities This contains a simple example of how to use the `ghostty-vt` paste -utilities to check if paste data is safe. +utilities to check if paste data is safe and encode it for terminal input. This uses a `build.zig` and `Zig` to build the C program so that we can reuse a lot of our build logic and depend directly on our source diff --git a/example/c-vt-paste/src/main.c b/example/c-vt-paste/src/main.c index bb9e8e2a5..e6e4b3d61 100644 --- a/example/c-vt-paste/src/main.c +++ b/example/c-vt-paste/src/main.c @@ -3,7 +3,7 @@ #include //! [paste-safety] -void basic_example() { +void safety_example() { const char* safe_data = "hello world"; const char* unsafe_data = "rm -rf /\n"; @@ -17,8 +17,26 @@ void basic_example() { } //! [paste-safety] +//! [paste-encode] +void encode_example() { + // The input buffer is modified in place (unsafe bytes are stripped). + char data[] = "hello\nworld"; + char buf[64]; + size_t written = 0; + + GhosttyResult result = ghostty_paste_encode( + data, strlen(data), true, buf, sizeof(buf), &written); + + if (result == GHOSTTY_SUCCESS) { + printf("Encoded %zu bytes: ", written); + fwrite(buf, 1, written, stdout); + printf("\n"); + } +} +//! [paste-encode] + int main() { - basic_example(); + safety_example(); // Test unsafe paste data with bracketed paste end sequence const char *unsafe_escape = "evil\x1b[201~code"; @@ -32,5 +50,7 @@ int main() { printf("Empty data is safe\n"); } + encode_example(); + return 0; } diff --git a/include/ghostty/vt/paste.h b/include/ghostty/vt/paste.h index b7212c801..bf91ca64b 100644 --- a/include/ghostty/vt/paste.h +++ b/include/ghostty/vt/paste.h @@ -9,22 +9,32 @@ /** @defgroup paste Paste Utilities * - * Utilities for validating paste data safety. + * Utilities for validating and encoding paste data for terminal input. * * ## Basic Usage * * Use ghostty_paste_is_safe() to check if paste data contains potentially * dangerous sequences before sending it to the terminal. * - * ## Example + * Use ghostty_paste_encode() to encode paste data for writing to the pty, + * including bracketed paste wrapping and unsafe byte stripping. + * + * ## Examples + * + * ### Safety Check * * @snippet c-vt-paste/src/main.c paste-safety * + * ### Encoding + * + * @snippet c-vt-paste/src/main.c paste-encode + * * @{ */ #include #include +#include #ifdef __cplusplus extern "C" { @@ -47,6 +57,41 @@ extern "C" { */ bool ghostty_paste_is_safe(const char* data, size_t len); +/** + * Encode paste data for writing to the terminal pty. + * + * This function prepares paste data for terminal input by: + * - Stripping unsafe control bytes (NUL, ESC, DEL, etc.) by replacing + * them with spaces + * - Wrapping the data in bracketed paste sequences if @p bracketed is true + * - Replacing newlines with carriage returns if @p bracketed is false + * + * The input @p data buffer is modified in place during encoding. The + * encoded result (potentially with bracketed paste prefix/suffix) is + * written to the output buffer. + * + * If the output buffer is too small, the function returns + * GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written. + * The caller can then retry with a sufficiently sized buffer. + * + * @param data The paste data to encode (modified in place, may be NULL) + * @param data_len The length of the input data in bytes + * @param bracketed Whether bracketed paste mode is active + * @param buf Output buffer to write the encoded result into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GhosttyResult ghostty_paste_encode( + char* data, + size_t data_len, + bool bracketed, + char* buf, + size_t buf_len, + size_t* out_written); + #ifdef __cplusplus } #endif diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 0a749be87..ba76fef53 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -167,6 +167,7 @@ comptime { @export(&c.focus_encode, .{ .name = "ghostty_focus_encode" }); @export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" }); @export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); + @export(&c.paste_encode, .{ .name = "ghostty_paste_encode" }); @export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" }); @export(&c.style_default, .{ .name = "ghostty_style_default" }); @export(&c.style_is_default, .{ .name = "ghostty_style_is_default" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 788790c69..661bff147 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -115,6 +115,7 @@ pub const mouse_encoder_reset = mouse_encode.reset; pub const mouse_encoder_encode = mouse_encode.encode; pub const paste_is_safe = paste.is_safe; +pub const paste_encode = paste.encode; pub const alloc_alloc = allocator.alloc; pub const alloc_free = allocator.free; diff --git a/src/terminal/c/paste.zig b/src/terminal/c/paste.zig index 69df416c0..bce6a5658 100644 --- a/src/terminal/c/paste.zig +++ b/src/terminal/c/paste.zig @@ -1,12 +1,104 @@ const std = @import("std"); const lib = @import("../lib.zig"); const paste = @import("../../input/paste.zig"); +const Result = @import("result.zig").Result; pub fn is_safe(data: ?[*]const u8, len: usize) callconv(lib.calling_conv) bool { const slice: []const u8 = if (data) |v| v[0..len] else &.{}; return paste.isSafe(slice); } +pub fn encode( + data: ?[*]u8, + data_len: usize, + bracketed: bool, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(lib.calling_conv) Result { + const slice: []u8 = if (data) |v| v[0..data_len] else &.{}; + const result = paste.encode(slice, .{ .bracketed = bracketed }); + + const total = result[0].len + result[1].len + result[2].len; + out_written.* = total; + + const out: []u8 = if (out_) |o| o[0..out_len] else &.{}; + if (out.len < total) return .out_of_space; + + var offset: usize = 0; + for (result) |segment| { + @memcpy(out[offset..][0..segment.len], segment); + offset += segment.len; + } + + return .success; +} + +test "encode bracketed" { + const testing = std.testing; + const input = try testing.allocator.dupe(u8, "hello"); + defer testing.allocator.free(input); + var buf: [64]u8 = undefined; + var written: usize = 0; + const result = encode(input.ptr, input.len, true, &buf, buf.len, &written); + try testing.expectEqual(.success, result); + try testing.expectEqualStrings("\x1b[200~hello\x1b[201~", buf[0..written]); +} + +test "encode unbracketed no newlines" { + const testing = std.testing; + const input = try testing.allocator.dupe(u8, "hello"); + defer testing.allocator.free(input); + var buf: [64]u8 = undefined; + var written: usize = 0; + const result = encode(input.ptr, input.len, false, &buf, buf.len, &written); + try testing.expectEqual(.success, result); + try testing.expectEqualStrings("hello", buf[0..written]); +} + +test "encode unbracketed newlines" { + const testing = std.testing; + const input = try testing.allocator.dupe(u8, "hello\nworld"); + defer testing.allocator.free(input); + var buf: [64]u8 = undefined; + var written: usize = 0; + const result = encode(input.ptr, input.len, false, &buf, buf.len, &written); + try testing.expectEqual(.success, result); + try testing.expectEqualStrings("hello\rworld", buf[0..written]); +} + +test "encode strip unsafe bytes" { + const testing = std.testing; + const input = try testing.allocator.dupe(u8, "hel\x1blo\x00world"); + defer testing.allocator.free(input); + var buf: [64]u8 = undefined; + var written: usize = 0; + const result = encode(input.ptr, input.len, true, &buf, buf.len, &written); + try testing.expectEqual(.success, result); + try testing.expectEqualStrings("\x1b[200~hel lo world\x1b[201~", buf[0..written]); +} + +test "encode with insufficient buffer" { + const testing = std.testing; + const input = try testing.allocator.dupe(u8, "hello"); + defer testing.allocator.free(input); + var buf: [1]u8 = undefined; + var written: usize = 0; + const result = encode(input.ptr, input.len, true, &buf, buf.len, &written); + try testing.expectEqual(.out_of_space, result); + try testing.expectEqual(17, written); +} + +test "encode with null buffer" { + const testing = std.testing; + const input = try testing.allocator.dupe(u8, "hello"); + defer testing.allocator.free(input); + var written: usize = 0; + const result = encode(input.ptr, input.len, true, null, 0, &written); + try testing.expectEqual(.out_of_space, result); + try testing.expectEqual(17, written); +} + test "is_safe with safe data" { const testing = std.testing; const safe = "hello world";