mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-21 17:21:52 +00:00
lib-vt: begin paste utilities exports starting with safe paste (#9068)
This commit is contained in:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
dir: [c-vt, zig-vt]
|
dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt]
|
||||||
name: Example ${{ matrix.dir }}
|
name: Example ${{ matrix.dir }}
|
||||||
runs-on: namespace-profile-ghostty-sm
|
runs-on: namespace-profile-ghostty-sm
|
||||||
needs: test
|
needs: test
|
||||||
|
17
example/c-vt-paste/README.md
Normal file
17
example/c-vt-paste/README.md
Normal file
@@ -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
|
||||||
|
```
|
42
example/c-vt-paste/build.zig
Normal file
42
example/c-vt-paste/build.zig
Normal file
@@ -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);
|
||||||
|
}
|
24
example/c-vt-paste/build.zig.zon
Normal file
24
example/c-vt-paste/build.zig.zon
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
31
example/c-vt-paste/src/main.c
Normal file
31
example/c-vt-paste/src/main.c
Normal file
@@ -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;
|
||||||
|
}
|
@@ -30,6 +30,7 @@
|
|||||||
* The API is organized into the following groups:
|
* The API is organized into the following groups:
|
||||||
* - @ref key "Key Encoding" - Encode key events into terminal sequences
|
* - @ref key "Key Encoding" - Encode key events into terminal sequences
|
||||||
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) 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
|
* - @ref allocator "Memory Management" - Memory management and custom allocators
|
||||||
*
|
*
|
||||||
* @section examples_sec Examples
|
* @section examples_sec Examples
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
* Complete working examples:
|
* Complete working examples:
|
||||||
* - @ref c-vt/src/main.c - OSC parser example
|
* - @ref c-vt/src/main.c - OSC parser example
|
||||||
* - @ref c-vt-key-encode/src/main.c - Key encoding 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.
|
* 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
|
#ifndef GHOSTTY_VT_H
|
||||||
#define GHOSTTY_VT_H
|
#define GHOSTTY_VT_H
|
||||||
|
|
||||||
@@ -61,6 +68,7 @@ extern "C" {
|
|||||||
#include <ghostty/vt/allocator.h>
|
#include <ghostty/vt/allocator.h>
|
||||||
#include <ghostty/vt/osc.h>
|
#include <ghostty/vt/osc.h>
|
||||||
#include <ghostty/vt/key.h>
|
#include <ghostty/vt/key.h>
|
||||||
|
#include <ghostty/vt/paste.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
75
include/ghostty/vt/paste.h
Normal file
75
include/ghostty/vt/paste.h
Normal file
@@ -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 */
|
@@ -122,6 +122,7 @@ comptime {
|
|||||||
@export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" });
|
@export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" });
|
||||||
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
|
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
|
||||||
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
|
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
|
||||||
|
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
pub const osc = @import("osc.zig");
|
pub const osc = @import("osc.zig");
|
||||||
pub const key_event = @import("key_event.zig");
|
pub const key_event = @import("key_event.zig");
|
||||||
pub const key_encode = @import("key_encode.zig");
|
pub const key_encode = @import("key_encode.zig");
|
||||||
|
pub const paste = @import("paste.zig");
|
||||||
|
|
||||||
// The full C API, unexported.
|
// The full C API, unexported.
|
||||||
pub const osc_new = osc.new;
|
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_setopt = key_encode.setopt;
|
||||||
pub const key_encoder_encode = key_encode.encode;
|
pub const key_encoder_encode = key_encode.encode;
|
||||||
|
|
||||||
|
pub const paste_is_safe = paste.is_safe;
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = osc;
|
_ = osc;
|
||||||
_ = key_event;
|
_ = key_event;
|
||||||
_ = key_encode;
|
_ = key_encode;
|
||||||
|
_ = paste;
|
||||||
|
|
||||||
// We want to make sure we run the tests for the C allocator interface.
|
// We want to make sure we run the tests for the C allocator interface.
|
||||||
_ = @import("../../lib/allocator.zig");
|
_ = @import("../../lib/allocator.zig");
|
||||||
|
36
src/terminal/c/paste.zig
Normal file
36
src/terminal/c/paste.zig
Normal file
@@ -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));
|
||||||
|
}
|
Reference in New Issue
Block a user