mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
libghostty: implement Kitty clipboard protocol read/write (#13963)
This implements the full Kitty clipboard protocol for libghostty-vt. libghostty users need to only have the clipboard read/write effect for this to work. This doesn't yet do mode 5522.
This commit is contained in:
@@ -95,8 +95,8 @@ extern "C" {
|
||||
* | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS query (CSI 14/16/18 t) or mode 2048 enable |
|
||||
* | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)|
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ` | `GhosttyTerminalClipboardReadFn` | Clipboard read via OSC 52 "?" |
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 / OSC 5522 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ` | `GhosttyTerminalClipboardReadFn` | Clipboard read via OSC 52 "?" / OSC 5522 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION`| `GhosttyTerminalDesktopNotificationFn` | Desktop notification via OSC 9 / OSC 777 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT` | `GhosttyTerminalProgressReportFn` | Progress report via OSC 9;4 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE` | `GhosttyTerminalUnknownSequenceFn` | Unsupported sequence identifier |
|
||||
@@ -494,7 +494,10 @@ typedef struct {
|
||||
* Result of a clipboard write callback.
|
||||
*
|
||||
* Protocols without write acknowledgements, including OSC 52 and iTerm2
|
||||
* OSC 1337 Copy, ignore this result.
|
||||
* OSC 1337 Copy, ignore this result. The Kitty clipboard protocol
|
||||
* (OSC 5522) acknowledges writes: each result maps to the corresponding
|
||||
* protocol status (DONE, EPERM, ENOSYS, EBUSY, EINVAL, EIO) and is
|
||||
* reported back to the running program through the write_pty callback.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
@@ -525,9 +528,18 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Called synchronously for a complete logical clipboard write. Protocol
|
||||
* details such as OSC 52 selectors, base64 encoding, multipart chunks,
|
||||
* aliases, and terminators are normalized before this callback is invoked.
|
||||
* OSC 52 and iTerm2 OSC 1337 Copy writes therefore use the same callback
|
||||
* shape. OSC 52 clipboard read requests ("?") are delivered to
|
||||
* GhosttyTerminalClipboardReadFn instead.
|
||||
* OSC 52, iTerm2 OSC 1337 Copy, and Kitty clipboard (OSC 5522) writes
|
||||
* therefore use the same callback shape.
|
||||
*
|
||||
* Every invocation is one complete write: the contents replace whatever
|
||||
* the destination previously held, so there is never a partial update to
|
||||
* detect or a reset to perform. A Kitty clipboard write transaction
|
||||
* results in exactly one invocation, at commit, carrying all of the
|
||||
* transaction's MIME representations together; its protocol response is
|
||||
* generated automatically from the returned result.
|
||||
*
|
||||
* Clipboard read requests (OSC 52 "?" and OSC 5522 reads) are delivered
|
||||
* to GhosttyTerminalClipboardReadFn instead.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
@@ -572,8 +584,9 @@ typedef enum {
|
||||
* duration of the reply call and may be freed as soon as it returns.
|
||||
*
|
||||
* Any result other than GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS answers the
|
||||
* program with an empty clipboard; the other fields are ignored in that
|
||||
* case. On success, `contents` should carry one representation per
|
||||
* program with an empty clipboard (OSC 52) or the matching protocol status
|
||||
* (OSC 5522: EPERM, ENOSYS, EBUSY, EIO); the other fields are ignored in
|
||||
* that case. On success, `contents` should carry one representation per
|
||||
* requested MIME type (GhosttyClipboardRead::mimes) that the clipboard
|
||||
* has; unrequested representations are ignored. Protocols that carry a
|
||||
* single text value (OSC 52) use the first entry with a text MIME type
|
||||
@@ -637,7 +650,7 @@ typedef void (*GhosttyClipboardReadReplyFn)(
|
||||
* GhosttyClipboardReadReply. This must happen before the callback returns;
|
||||
* the request is invalid afterwards. Calling `reply` more than once is
|
||||
* ignored. Returning without replying answers the program with an empty
|
||||
* clipboard.
|
||||
* clipboard (OSC 52) or EPERM (OSC 5522).
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
@@ -695,15 +708,22 @@ struct GhosttyClipboardRead {
|
||||
* Callback function type for clipboard_read.
|
||||
*
|
||||
* Called synchronously when the running program requests clipboard contents
|
||||
* via OSC 52 with a "?" payload. Answering lets the program read the user's
|
||||
* clipboard, so the embedder is expected to mediate consent. Because the
|
||||
* read is synchronous, an embedder that needs to ask the user must block
|
||||
* (for example by running a modal prompt) until it has an answer; the VT
|
||||
* stream waits until the callback returns.
|
||||
* via OSC 52 with a "?" payload or a Kitty clipboard (OSC 5522) read.
|
||||
* Answering lets the program read the user's clipboard, so the embedder is
|
||||
* expected to mediate consent. Because the read is synchronous, an embedder
|
||||
* that needs to ask the user must block (for example by running a modal
|
||||
* prompt) until it has an answer; the VT stream waits until the callback
|
||||
* returns.
|
||||
*
|
||||
* Answer by calling `read->reply(read, &reply)` before returning. See
|
||||
* GhosttyClipboardRead for the full contract.
|
||||
*
|
||||
* OSC 5522 requests carry the program's MIME list, name, and password grant
|
||||
* state; a reply that sets `remember` records a session grant so later
|
||||
* requests with the same password arrive with `granted` set. Kitty itself
|
||||
* serves a request for only the targets listing (`list` with no `mimes`)
|
||||
* without prompting.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param read Borrowed clipboard read request
|
||||
@@ -1243,9 +1263,10 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
|
||||
/**
|
||||
* Callback invoked when the running program performs a clipboard write.
|
||||
* OSC 52 and iTerm2 OSC 1337 Copy writes are normalized to an atomic set
|
||||
* of decoded MIME representations. Set to NULL to ignore clipboard writes.
|
||||
* Clipboard read requests are delivered to
|
||||
* OSC 52, iTerm2 OSC 1337 Copy, and Kitty clipboard (OSC 5522) writes
|
||||
* are normalized to an atomic set of decoded MIME representations. Set
|
||||
* to NULL to ignore clipboard writes (Kitty clipboard writes are then
|
||||
* refused with ENOSYS). Clipboard read requests are delivered to
|
||||
* GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ instead.
|
||||
*
|
||||
* Input type: GhosttyTerminalClipboardWriteFn
|
||||
@@ -1411,9 +1432,10 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
|
||||
/**
|
||||
* Callback invoked when the running program requests clipboard contents
|
||||
* via OSC 52 with a "?" payload. The read is synchronous and must be
|
||||
* answered before the callback returns. Set to NULL to ignore clipboard
|
||||
* read requests (the default).
|
||||
* via OSC 52 with a "?" payload or a Kitty clipboard (OSC 5522) read. The
|
||||
* read is synchronous and must be answered before the callback returns.
|
||||
* Set to NULL (the default) to ignore OSC 52 read requests and refuse
|
||||
* OSC 5522 reads with EPERM.
|
||||
*
|
||||
* Input type: GhosttyTerminalClipboardReadFn
|
||||
*/
|
||||
|
||||
@@ -50,10 +50,10 @@ pub const sys = terminal.sys;
|
||||
pub const TinyIo = @import("lib/TinyIo.zig");
|
||||
|
||||
pub const apc = terminal.apc;
|
||||
pub const clipboard = terminal.clipboard;
|
||||
pub const dcs = terminal.dcs;
|
||||
pub const osc = terminal.osc;
|
||||
pub const point = terminal.point;
|
||||
pub const clipboard = terminal.clipboard;
|
||||
pub const color = terminal.color;
|
||||
pub const device_status = terminal.device_status;
|
||||
pub const formatter = terminal.formatter;
|
||||
|
||||
@@ -240,8 +240,12 @@ pub const ModeConfig = extern struct {
|
||||
|
||||
/// C callback state for terminal effects. Most trampolines are always
|
||||
/// installed on the stream handler; they check these fields and no-op when
|
||||
/// the corresponding callback is null. The unknown-sequence trampoline is
|
||||
/// installed dynamically to preserve its null fast path.
|
||||
/// the corresponding callback is null. The unknown-sequence and
|
||||
/// clipboard trampolines are installed dynamically to preserve their
|
||||
/// null fast paths (for clipboard_write, a null Zig-level effect makes
|
||||
/// Kitty clipboard writes fail up front instead of spooling a
|
||||
/// transaction that can never commit; for clipboard_read it keeps
|
||||
/// reads denied).
|
||||
const Effects = struct {
|
||||
userdata: ?*anyopaque = null,
|
||||
write_pty: ?WritePtyFn = null,
|
||||
@@ -660,7 +664,9 @@ fn wrap(
|
||||
.pwd_changed = &Effects.pwdChangedTrampoline,
|
||||
.progress_report = &Effects.progressReportTrampoline,
|
||||
.size = &Effects.sizeTrampoline,
|
||||
.clipboard_write = &Effects.clipboardWriteTrampoline,
|
||||
|
||||
// Installed dynamically when the callback is set; see Effects.
|
||||
.clipboard_write = null,
|
||||
.clipboard_read = null,
|
||||
};
|
||||
|
||||
@@ -1244,7 +1250,13 @@ fn setTyped(
|
||||
.pwd_changed => wrapper.effects.pwd_changed = value,
|
||||
.progress_report => wrapper.effects.progress_report = value,
|
||||
.size_cb => wrapper.effects.size_cb = value,
|
||||
.clipboard_write => wrapper.effects.clipboard_write = value,
|
||||
.clipboard_write => {
|
||||
wrapper.effects.clipboard_write = value;
|
||||
wrapper.stream.handler.effects.clipboard_write = if (value != null)
|
||||
&Effects.clipboardWriteTrampoline
|
||||
else
|
||||
null;
|
||||
},
|
||||
.clipboard_read => {
|
||||
wrapper.effects.clipboard_read = value;
|
||||
wrapper.stream.handler.effects.clipboard_read = if (value != null)
|
||||
@@ -4717,8 +4729,10 @@ test "set clipboard_write callback" {
|
||||
try testing.expectEqualStrings("image/png", S.last_mimes[4][0..S.last_mime_lens[4]]);
|
||||
try testing.expectEqualSlices(u8, "\x89PNG", S.last_data[4][0..S.last_data_lens[4]]);
|
||||
|
||||
// Removing the callback takes effect immediately.
|
||||
// Removing the callback takes effect immediately and uninstalls
|
||||
// the trampoline.
|
||||
try testing.expectEqual(Result.success, set(t, .clipboard_write, null));
|
||||
try testing.expect(t.?.stream.handler.effects.clipboard_write == null);
|
||||
const after_remove = "\x1B]52;c;eA==\x1B\\";
|
||||
vt_write(t, after_remove, after_remove.len);
|
||||
try testing.expectEqual(@as(usize, 7), S.count);
|
||||
@@ -4738,12 +4752,183 @@ test "clipboard_write without callback is unsupported and silent" {
|
||||
const seq = "\x1B]52;c;aGVsbG8=\x1B\\";
|
||||
vt_write(t, seq, seq.len);
|
||||
|
||||
const handler = &t.?.stream.handler;
|
||||
const result = handler.effects.clipboard_write.?(handler, .{
|
||||
.location = .standard,
|
||||
.contents = &.{.{ .mime = "text/plain", .data = "hello" }},
|
||||
});
|
||||
try testing.expectEqual(clipboard.WriteResult.unsupported, result);
|
||||
// No trampoline is installed until a callback is set, so the
|
||||
// stream skips clipboard work (and never spools a Kitty clipboard
|
||||
// transaction it can't deliver).
|
||||
try testing.expect(t.?.stream.handler.effects.clipboard_write == null);
|
||||
}
|
||||
|
||||
test "kitty clipboard write via C effects" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
80,
|
||||
24,
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
const S = struct {
|
||||
var responses: [512]u8 = undefined;
|
||||
var responses_len: usize = 0;
|
||||
var write_count: usize = 0;
|
||||
var last_location: clipboard.Location = .standard;
|
||||
var last_contents_len: usize = 0;
|
||||
var last_mimes: [4][64]u8 = undefined;
|
||||
var last_mime_lens: [4]usize = @splat(0);
|
||||
var last_data: [4][64]u8 = undefined;
|
||||
var last_data_lens: [4]usize = @splat(0);
|
||||
|
||||
fn writePty(
|
||||
_: Terminal,
|
||||
_: ?*anyopaque,
|
||||
ptr: [*]const u8,
|
||||
len: usize,
|
||||
) callconv(lib.calling_conv) void {
|
||||
@memcpy(responses[responses_len..][0..len], ptr[0..len]);
|
||||
responses_len += len;
|
||||
}
|
||||
|
||||
fn clipboardWrite(
|
||||
_: Terminal,
|
||||
_: ?*anyopaque,
|
||||
request: *const ClipboardWrite,
|
||||
) callconv(lib.calling_conv) clipboard.WriteResult {
|
||||
write_count += 1;
|
||||
last_location = request.location;
|
||||
last_contents_len = request.contents_len;
|
||||
if (request.contents) |ptr| {
|
||||
for (ptr[0..@min(request.contents_len, last_mimes.len)], 0..) |content, i| {
|
||||
last_mime_lens[i] = @min(content.mime.len, last_mimes[i].len);
|
||||
@memcpy(
|
||||
last_mimes[i][0..last_mime_lens[i]],
|
||||
content.mime.ptr[0..last_mime_lens[i]],
|
||||
);
|
||||
last_data_lens[i] = @min(content.data.len, last_data[i].len);
|
||||
@memcpy(
|
||||
last_data[i][0..last_data_lens[i]],
|
||||
content.data.ptr[0..last_data_lens[i]],
|
||||
);
|
||||
}
|
||||
}
|
||||
return .success;
|
||||
}
|
||||
};
|
||||
S.responses_len = 0;
|
||||
S.write_count = 0;
|
||||
S.last_mime_lens = @splat(0);
|
||||
S.last_data_lens = @splat(0);
|
||||
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
try testing.expectEqual(Result.success, set(t, .clipboard_write, @ptrCast(&S.clipboardWrite)));
|
||||
|
||||
// A full OSC 5522 write transaction: begin, chunked data for two
|
||||
// representations, commit. Only the commit invokes the callback,
|
||||
// and its result maps to the DONE response.
|
||||
const seqs = [_][]const u8{
|
||||
"\x1B]5522;type=write:id=c1\x1B\\",
|
||||
"\x1B]5522;type=wdata:mime=dGV4dC9wbGFpbg==;R2hvc3Q=\x1B\\", // "Ghost"
|
||||
"\x1B]5522;type=wdata:mime=dGV4dC9wbGFpbg==;dHk=\x1B\\", // "ty"
|
||||
"\x1B]5522;type=wdata:mime=dGV4dC9odG1s;PGI+aGk8L2I+\x1B\\", // "<b>hi</b>"
|
||||
"\x1B]5522;type=wdata\x1B\\",
|
||||
};
|
||||
for (seqs) |seq| vt_write(t, seq.ptr, seq.len);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqual(clipboard.Location.standard, S.last_location);
|
||||
try testing.expectEqual(@as(usize, 2), S.last_contents_len);
|
||||
try testing.expectEqualStrings("text/plain", S.last_mimes[0][0..S.last_mime_lens[0]]);
|
||||
try testing.expectEqualStrings("Ghostty", S.last_data[0][0..S.last_data_lens[0]]);
|
||||
try testing.expectEqualStrings("text/html", S.last_mimes[1][0..S.last_mime_lens[1]]);
|
||||
try testing.expectEqualStrings("<b>hi</b>", S.last_data[1][0..S.last_data_lens[1]]);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=write:status=DONE:id=c1\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
);
|
||||
|
||||
// Without a read callback reads are denied.
|
||||
S.responses_len = 0;
|
||||
const read = "\x1B]5522;type=read:id=r1;dGV4dC9wbGFpbg==\x1B\\";
|
||||
vt_write(t, read, read.len);
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=read:status=EPERM:id=r1\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
);
|
||||
|
||||
// With a read callback the request is served through it.
|
||||
const R = struct {
|
||||
var count: usize = 0;
|
||||
var last_mimes_len: usize = 0;
|
||||
var last_mime_is_text: bool = false;
|
||||
var last_list: bool = true;
|
||||
var last_name_len: usize = 0;
|
||||
var last_granted: bool = true;
|
||||
var last_can_remember: bool = true;
|
||||
|
||||
fn clipboardRead(
|
||||
_: Terminal,
|
||||
_: ?*anyopaque,
|
||||
request: *const ClipboardRead,
|
||||
) callconv(lib.calling_conv) void {
|
||||
count += 1;
|
||||
last_mimes_len = request.mimes_len;
|
||||
last_mime_is_text = request.mimes_len > 0 and std.mem.eql(
|
||||
u8,
|
||||
request.mimes.?[0].ptr[0..request.mimes.?[0].len],
|
||||
"text/plain",
|
||||
);
|
||||
last_list = request.list;
|
||||
last_name_len = request.name.len;
|
||||
last_granted = request.granted;
|
||||
last_can_remember = request.can_remember;
|
||||
|
||||
const mime: []const u8 = "text/plain";
|
||||
const data: []const u8 = "hello";
|
||||
const contents = [_]ClipboardContent{.{
|
||||
.mime = .init(mime),
|
||||
.data = .init(data),
|
||||
}};
|
||||
request.reply(request, &.{
|
||||
.size = @sizeOf(ClipboardReadReply),
|
||||
.result = .success,
|
||||
.contents = &contents,
|
||||
.contents_len = contents.len,
|
||||
.available = null,
|
||||
.available_len = 0,
|
||||
.remember = false,
|
||||
});
|
||||
}
|
||||
};
|
||||
try testing.expectEqual(Result.success, set(t, .clipboard_read, @ptrCast(&R.clipboardRead)));
|
||||
S.responses_len = 0;
|
||||
// name="app" without a password: forwarded for prompts, not
|
||||
// rememberable.
|
||||
const read2 = "\x1B]5522;type=read:id=r2:name=YXBw;dGV4dC9wbGFpbg==\x1B\\";
|
||||
vt_write(t, read2, read2.len);
|
||||
try testing.expectEqual(@as(usize, 1), R.count);
|
||||
try testing.expectEqual(@as(usize, 1), R.last_mimes_len);
|
||||
try testing.expect(R.last_mime_is_text);
|
||||
try testing.expect(!R.last_list);
|
||||
try testing.expectEqual(@as(usize, 3), R.last_name_len);
|
||||
try testing.expect(!R.last_granted);
|
||||
try testing.expect(!R.last_can_remember);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=read:status=OK:id=r2\x1B\\" ++
|
||||
"\x1B]5522;type=read:status=DATA:id=r2:mime=dGV4dC9wbGFpbg==;aGVsbG8=\x1B\\" ++
|
||||
"\x1B]5522;type=read:status=DONE:id=r2\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
);
|
||||
|
||||
// Without a clipboard callback the transaction fails up front.
|
||||
try testing.expectEqual(Result.success, set(t, .clipboard_write, null));
|
||||
S.responses_len = 0;
|
||||
const begin = "\x1B]5522;type=write:id=c2\x1B\\";
|
||||
vt_write(t, begin, begin.len);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=write:status=ENOSYS:id=c2\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
);
|
||||
}
|
||||
|
||||
test "set clipboard_read callback" {
|
||||
|
||||
@@ -28,8 +28,8 @@ pub const max_pw_len = 128;
|
||||
/// types are tiny; anything longer drops the packet.
|
||||
pub const max_mime_len = 256;
|
||||
|
||||
/// Maximum decoded name length we bother validating. Longer names are
|
||||
/// treated as present without validation; only their presence matters.
|
||||
/// Maximum decoded name length. Kitty has no limit but names are shown
|
||||
/// in permission prompts so anything longer drops the packet.
|
||||
pub const max_name_len = 256;
|
||||
|
||||
/// The decoded, validated metadata of one OSC 5522 sequence.
|
||||
@@ -62,9 +62,10 @@ pub const Metadata = struct {
|
||||
/// treat the request as though it had no password."
|
||||
pw: []const u8 = "",
|
||||
|
||||
/// True if a non-empty (valid) name was given. We don't retain the
|
||||
/// name contents; it exists to opt into password grants.
|
||||
has_name: bool = false,
|
||||
/// Decoded human friendly name of the requesting program, shown in
|
||||
/// permission prompts. Empty means absent. Its presence opts into
|
||||
/// password grants.
|
||||
name: []const u8 = "",
|
||||
|
||||
/// Parse the metadata field. The raw value is expected to be exactly
|
||||
/// the metadata (prefix and payload and separators stripped out).
|
||||
@@ -124,21 +125,13 @@ pub const Metadata = struct {
|
||||
error.Invalid => return null,
|
||||
};
|
||||
} else if (std.mem.eql(u8, key, "name")) {
|
||||
// We only need to know whether a (non-empty) name was
|
||||
// given; the contents are decoded for validation only.
|
||||
result.has_name = has_name: {
|
||||
const name = decodeValue(
|
||||
alloc,
|
||||
value,
|
||||
max_name_len,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
// Over-long names are accepted as present but
|
||||
// not validated further.
|
||||
error.Overflow => break :has_name true,
|
||||
error.Invalid => return null,
|
||||
};
|
||||
break :has_name name.len > 0;
|
||||
result.name = decodeValue(
|
||||
alloc,
|
||||
value,
|
||||
max_name_len,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Overflow, error.Invalid => return null,
|
||||
};
|
||||
}
|
||||
// Unknown keys are ignored.
|
||||
@@ -351,7 +344,21 @@ test "metadata: pw and name" {
|
||||
// pw="secret", name="app"
|
||||
const meta = (try Metadata.parse(arena.allocator(), "type=read:pw=c2VjcmV0:name=YXBw")).?;
|
||||
try testing.expectEqualStrings("secret", meta.pw);
|
||||
try testing.expect(meta.has_name);
|
||||
try testing.expectEqualStrings("app", meta.name);
|
||||
}
|
||||
|
||||
test "metadata: over-long name dropped" {
|
||||
const testing = std.testing;
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const Encoder = std.base64.standard.Encoder;
|
||||
const long = "n" ** (max_name_len + 1);
|
||||
var buf: [Encoder.calcSize(long.len)]u8 = undefined;
|
||||
const raw = try std.mem.concat(arena.allocator(), u8, &.{
|
||||
"type=read:name=",
|
||||
Encoder.encode(&buf, long),
|
||||
});
|
||||
try testing.expect((try Metadata.parse(arena.allocator(), raw)) == null);
|
||||
}
|
||||
|
||||
test "metadata: empty name" {
|
||||
@@ -359,7 +366,7 @@ test "metadata: empty name" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const meta = (try Metadata.parse(arena.allocator(), "type=read:pw=c2VjcmV0:name=")).?;
|
||||
try testing.expect(!meta.has_name);
|
||||
try testing.expectEqual(@as(usize, 0), meta.name.len);
|
||||
}
|
||||
|
||||
test "payload: mime iterator" {
|
||||
|
||||
@@ -36,7 +36,7 @@ pub const WriteState = struct {
|
||||
loc: clipboard.Location,
|
||||
id: []const u8,
|
||||
pw: []const u8,
|
||||
has_name: bool,
|
||||
name: []const u8,
|
||||
spool: std.ArrayListUnmanaged(u8) = .empty,
|
||||
entries: std.ArrayListUnmanaged(Entry) = .empty,
|
||||
aliases: std.ArrayListUnmanaged(Alias) = .empty,
|
||||
@@ -68,12 +68,13 @@ pub const WriteState = struct {
|
||||
errdefer arena.deinit();
|
||||
const id = try arena.allocator().dupe(u8, meta.id);
|
||||
const pw = try arena.allocator().dupe(u8, meta.pw);
|
||||
const name = try arena.allocator().dupe(u8, meta.name);
|
||||
return .{
|
||||
.arena = arena,
|
||||
.loc = meta.loc,
|
||||
.id = id,
|
||||
.pw = pw,
|
||||
.has_name = meta.has_name,
|
||||
.name = name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,7 +221,7 @@ pub const WriteState = struct {
|
||||
loc: clipboard.Location,
|
||||
id: []const u8,
|
||||
pw: []const u8,
|
||||
has_name: bool,
|
||||
name: []const u8,
|
||||
truncated: bool,
|
||||
contents: []const Content,
|
||||
|
||||
@@ -289,7 +290,7 @@ pub const WriteState = struct {
|
||||
.loc = self.loc,
|
||||
.id = self.id,
|
||||
.pw = self.pw,
|
||||
.has_name = self.has_name,
|
||||
.name = self.name,
|
||||
.truncated = self.truncated,
|
||||
.contents = try contents.toOwnedSlice(alloc),
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user