From bf9f025aec78aedcb9431d503fd6c4b14f579fbb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 21:04:18 -0700 Subject: [PATCH] lib-vt: begin paste utilities exports starting with safe paste --- .github/workflows/test.yml | 2 +- example/c-vt-paste/README.md | 17 ++++++++ example/c-vt-paste/build.zig | 42 ++++++++++++++++++ example/c-vt-paste/build.zig.zon | 24 ++++++++++ example/c-vt-paste/src/main.c | 31 +++++++++++++ include/ghostty/vt.h | 8 ++++ include/ghostty/vt/paste.h | 75 ++++++++++++++++++++++++++++++++ src/lib_vt.zig | 1 + src/terminal/c/main.zig | 4 ++ src/terminal/c/paste.zig | 36 +++++++++++++++ 10 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 example/c-vt-paste/README.md create mode 100644 example/c-vt-paste/build.zig create mode 100644 example/c-vt-paste/build.zig.zon create mode 100644 example/c-vt-paste/src/main.c create mode 100644 include/ghostty/vt/paste.h create mode 100644 src/terminal/c/paste.zig diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59556f58e..f78855290 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,7 +94,7 @@ jobs: strategy: fail-fast: false matrix: - dir: [c-vt, zig-vt] + dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt] name: Example ${{ matrix.dir }} runs-on: namespace-profile-ghostty-sm needs: test diff --git a/example/c-vt-paste/README.md b/example/c-vt-paste/README.md new file mode 100644 index 000000000..0f911771f --- /dev/null +++ b/example/c-vt-paste/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` Paste Safety Check + +This contains a simple example of how to use the `ghostty-vt` paste +utilities to check if paste data is safe. + +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 +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-paste/build.zig b/example/c-vt-paste/build.zig new file mode 100644 index 000000000..99b7ba771 --- /dev/null +++ b/example/c-vt-paste/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_paste", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-paste/build.zig.zon b/example/c-vt-paste/build.zig.zon new file mode 100644 index 000000000..fb78db9bc --- /dev/null +++ b/example/c-vt-paste/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_paste, + .version = "0.0.0", + .fingerprint = 0xa105002abbc8cf74, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-paste/src/main.c b/example/c-vt-paste/src/main.c new file mode 100644 index 000000000..153861ca9 --- /dev/null +++ b/example/c-vt-paste/src/main.c @@ -0,0 +1,31 @@ +#include +#include +#include + +int main() { + // Test safe paste data + const char *safe_data = "hello world"; + if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + printf("'%s' is safe to paste\n", safe_data); + } + + // Test unsafe paste data with newline + const char *unsafe_newline = "rm -rf /\n"; + if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) { + printf("'%s' is UNSAFE - contains newline\n", unsafe_newline); + } + + // Test unsafe paste data with bracketed paste end sequence + const char *unsafe_escape = "evil\x1b[201~code"; + if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) { + printf("Data with escape sequence is UNSAFE\n"); + } + + // Test empty data + const char *empty_data = ""; + if (ghostty_paste_is_safe(empty_data, 0)) { + printf("Empty data is safe\n"); + } + + return 0; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 489996530..cd357f0fa 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -30,6 +30,7 @@ * The API is organized into the following groups: * - @ref key "Key Encoding" - Encode key events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref paste "Paste Utilities" - Validate paste data safety * - @ref allocator "Memory Management" - Memory management and custom allocators * * @section examples_sec Examples @@ -37,6 +38,7 @@ * Complete working examples: * - @ref c-vt/src/main.c - OSC parser example * - @ref c-vt-key-encode/src/main.c - Key encoding example + * - @ref c-vt-paste/src/main.c - Paste safety check example * */ @@ -50,6 +52,11 @@ * into terminal escape sequences using the Kitty keyboard protocol. */ +/** @example c-vt-paste/src/main.c + * This example demonstrates how to use the paste utilities to check if + * paste data is safe before sending it to the terminal. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -61,6 +68,7 @@ extern "C" { #include #include #include +#include #ifdef __cplusplus } diff --git a/include/ghostty/vt/paste.h b/include/ghostty/vt/paste.h new file mode 100644 index 000000000..d90f303d4 --- /dev/null +++ b/include/ghostty/vt/paste.h @@ -0,0 +1,75 @@ +/** + * @file paste.h + * + * Paste utilities - validate and encode paste data for terminal input. + */ + +#ifndef GHOSTTY_VT_PASTE_H +#define GHOSTTY_VT_PASTE_H + +/** @defgroup paste Paste Utilities + * + * Utilities for validating paste data safety. + * + * ## Basic Usage + * + * Use ghostty_paste_is_safe() to check if paste data contains potentially + * dangerous sequences before sending it to the terminal. + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * const char* safe_data = "hello world"; + * const char* unsafe_data = "rm -rf /\n"; + * + * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + * printf("Safe to paste\n"); + * } + * + * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { + * printf("Unsafe! Contains newline\n"); + * } + * + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Check if paste data is safe to paste into the terminal. + * + * Data is considered unsafe if it contains: + * - Newlines (`\n`) which can inject commands + * - The bracketed paste end sequence (`\x1b[201~`) which can be used + * to exit bracketed paste mode and inject commands + * + * This check is conservative and considers data unsafe regardless of + * current terminal state. + * + * @param data The paste data to check (must not be NULL) + * @param len The length of the data in bytes + * @return true if the data is safe to paste, false otherwise + */ +bool ghostty_paste_is_safe(const char* data, size_t len); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_PASTE_H */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 73a030333..1df8330ea 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -122,6 +122,7 @@ comptime { @export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" }); @export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" }); @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); + @export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); } } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 500dbf56c..f68333d9b 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,6 +1,7 @@ pub const osc = @import("osc.zig"); pub const key_event = @import("key_event.zig"); pub const key_encode = @import("key_encode.zig"); +pub const paste = @import("paste.zig"); // The full C API, unexported. pub const osc_new = osc.new; @@ -33,10 +34,13 @@ pub const key_encoder_free = key_encode.free; pub const key_encoder_setopt = key_encode.setopt; pub const key_encoder_encode = key_encode.encode; +pub const paste_is_safe = paste.is_safe; + test { _ = osc; _ = key_event; _ = key_encode; + _ = paste; // We want to make sure we run the tests for the C allocator interface. _ = @import("../../lib/allocator.zig"); diff --git a/src/terminal/c/paste.zig b/src/terminal/c/paste.zig new file mode 100644 index 000000000..eb4117a70 --- /dev/null +++ b/src/terminal/c/paste.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const paste = @import("../../input/paste.zig"); + +pub fn is_safe(data: ?[*]const u8, len: usize) callconv(.c) bool { + const slice: []const u8 = if (data) |v| v[0..len] else &.{}; + return paste.isSafe(slice); +} + +test "is_safe with safe data" { + const testing = std.testing; + const safe = "hello world"; + try testing.expect(is_safe(safe.ptr, safe.len)); +} + +test "is_safe with newline" { + const testing = std.testing; + const unsafe = "hello\nworld"; + try testing.expect(!is_safe(unsafe.ptr, unsafe.len)); +} + +test "is_safe with bracketed paste end" { + const testing = std.testing; + const unsafe = "hello\x1b[201~world"; + try testing.expect(!is_safe(unsafe.ptr, unsafe.len)); +} + +test "is_safe with empty data" { + const testing = std.testing; + const empty = ""; + try testing.expect(is_safe(empty.ptr, 0)); +} + +test "is_safe with null empty data" { + const testing = std.testing; + try testing.expect(is_safe(null, 0)); +}