summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitchell Hashimoto <m@mitchellh.com>2025-10-06 21:13:54 -0700
committerGitHub <noreply@github.com>2025-10-06 21:13:54 -0700
commit67ece534237ea5ebf99f2fe5628c56d52e3291f3 (patch)
tree3d0d82e9cc53d6bf3800d581c04b5d23cb8dfba5
parent5ece02fa76a6aefc0c617dc0ec27624951ef502b (diff)
parentbf9f025aec78aedcb9431d503fd6c4b14f579fbb (diff)
lib-vt: begin paste utilities exports starting with safe paste (#9068)
-rw-r--r--.github/workflows/test.yml2
-rw-r--r--example/c-vt-paste/README.md17
-rw-r--r--example/c-vt-paste/build.zig42
-rw-r--r--example/c-vt-paste/build.zig.zon24
-rw-r--r--example/c-vt-paste/src/main.c31
-rw-r--r--include/ghostty/vt.h8
-rw-r--r--include/ghostty/vt/paste.h75
-rw-r--r--src/lib_vt.zig1
-rw-r--r--src/terminal/c/main.zig4
-rw-r--r--src/terminal/c/paste.zig36
10 files changed, 239 insertions, 1 deletions
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 <stdio.h>
+#include <string.h>
+#include <ghostty/vt.h>
+
+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 <ghostty/vt/allocator.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/key.h>
+#include <ghostty/vt/paste.h>
#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 <stdio.h>
+ * #include <string.h>
+ * #include <ghostty/vt.h>
+ *
+ * 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 <stdbool.h>
+#include <stddef.h>
+
+#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));
+}