1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
const std = @import("std");
const assert = std.debug.assert;
const Terminal = @import("../terminal/Terminal.zig");
pub const Options = struct {
/// True if bracketed paste mode is on.
bracketed: bool,
/// Return the encoding options based on the current terminal state.
pub fn fromTerminal(t: *const Terminal) Options {
return .{
.bracketed = t.modes.get(.bracketed_paste),
};
}
};
/// Encode the given data for pasting. The resulting value can be written
/// to the pty to perform a paste of the input data.
///
/// The data can be either a `[]u8` or a `[]const u8`. If the data
/// type is const then `EncodeError` may be returned. If the data type
/// is mutable then this function can't return an error.
///
/// This slightly complex calling style allows for initially const
/// data to be passed in without an allocation, since it is rare in normal
/// use cases that the data will need to be modified. In the unlikely case
/// data does need to be modified, the caller can make a mutable copy
/// after seeing the error.
///
/// The data is returned as a set of slices to limit allocations. The caller
/// can combine the slices into a single buffer if desired.
///
/// WARNING: The input data is not checked for safety. See the `isSafe`
/// function to check if the data is safe to paste.
pub fn encode(
data: anytype,
opts: Options,
) switch (@TypeOf(data)) {
[]u8 => [3][]const u8,
[]const u8 => Error![3][]const u8,
else => unreachable,
} {
const mutable = @TypeOf(data) == []u8;
var result: [3][]const u8 = .{ "", data, "" };
// Bracketed paste mode (mode 2004) wraps pasted data in
// fenceposts so that the terminal can ignore things like newlines.
if (opts.bracketed) {
result[0] = "\x1b[200~";
result[2] = "\x1b[201~";
return result;
}
// Non-bracketed. We have to replace newline with `\r`. This matches
// the behavior of xterm and other terminals. For `\r\n` this will
// result in `\r\r` which does match xterm.
if (comptime mutable) {
std.mem.replaceScalar(u8, data, '\n', '\r');
} else if (std.mem.indexOfScalar(u8, data, '\n') != null) {
return Error.MutableRequired;
}
return result;
}
pub const Error = error{
/// Returned if encoding requires a mutable copy of the data. This
/// can only be returned if the input data type is const.
MutableRequired,
};
/// Returns true if the data looks safe to paste. Data is considered
/// unsafe if it contains any of the following:
///
/// - `\n`: Newlines can be used to inject commands.
/// - `\x1b[201~`: This is the end of a bracketed paste. This cane be used
/// to exit a bracketed paste and inject commands.
///
/// We consider any scenario unsafe regardless of current terminal state.
/// For example, even if bracketed paste mode is not active, we still
/// consider `\x1b[201~` unsafe. The existence of these types of bytes
/// should raise suspicion that the producer of the paste data is
/// acting strangely.
pub fn isSafe(data: []const u8) bool {
return std.mem.indexOf(u8, data, "\n") == null and
std.mem.indexOf(u8, data, "\x1b[201~") == null;
}
test isSafe {
const testing = std.testing;
try testing.expect(isSafe("hello"));
try testing.expect(!isSafe("hello\n"));
try testing.expect(!isSafe("hello\nworld"));
try testing.expect(!isSafe("he\x1b[201~llo"));
}
test "encode bracketed" {
const testing = std.testing;
const result = try encode(
@as([]const u8, "hello"),
.{ .bracketed = true },
);
try testing.expectEqualStrings("\x1b[200~", result[0]);
try testing.expectEqualStrings("hello", result[1]);
try testing.expectEqualStrings("\x1b[201~", result[2]);
}
test "encode unbracketed no newlines" {
const testing = std.testing;
const result = try encode(
@as([]const u8, "hello"),
.{ .bracketed = false },
);
try testing.expectEqualStrings("", result[0]);
try testing.expectEqualStrings("hello", result[1]);
try testing.expectEqualStrings("", result[2]);
}
test "encode unbracketed newlines const" {
const testing = std.testing;
try testing.expectError(Error.MutableRequired, encode(
@as([]const u8, "hello\nworld"),
.{ .bracketed = false },
));
}
test "encode unbracketed newlines" {
const testing = std.testing;
const data: []u8 = try testing.allocator.dupe(u8, "hello\nworld");
defer testing.allocator.free(data);
const result = encode(data, .{ .bracketed = false });
try testing.expectEqualStrings("", result[0]);
try testing.expectEqualStrings("hello\rworld", result[1]);
try testing.expectEqualStrings("", result[2]);
}
test "encode unbracketed windows-stye newline" {
const testing = std.testing;
const data: []u8 = try testing.allocator.dupe(u8, "hello\r\nworld");
defer testing.allocator.free(data);
const result = encode(data, .{ .bracketed = false });
try testing.expectEqualStrings("", result[0]);
try testing.expectEqualStrings("hello\r\rworld", result[1]);
try testing.expectEqualStrings("", result[2]);
}
|