Skip to content

Commit 25b7b66

Browse files
committed
add bufPrintLogMessage function
1 parent ada238d commit 25b7b66

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

src/lsp.zig

+154
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,160 @@ pub const AnyTransport = struct {
13091309
}
13101310
};
13111311

1312+
pub const minimum_logging_buffer_size: usize = 128;
1313+
1314+
/// Creates a `window/logMessage` notification.
1315+
/// Returns a slice that points into `buffer`.
1316+
pub fn bufPrintLogMessage(
1317+
/// A temporary buffer which will be used to store the json message.
1318+
/// The formatted message will be truncated to fit into the buffer.
1319+
///
1320+
/// Must be at least `minimum_logging_buffer_size` bytes long.
1321+
buffer: []u8,
1322+
message_type: types.MessageType,
1323+
comptime fmt: []const u8,
1324+
args: anytype,
1325+
) []u8 {
1326+
return bufPrintLogMessageTypeErased(
1327+
buffer,
1328+
message_type,
1329+
struct {
1330+
fn format(writer: std.io.AnyWriter, opaque_params: *const anyopaque) void {
1331+
std.fmt.format(writer, fmt, @as(*const @TypeOf(args), @alignCast(@ptrCast(opaque_params))).*) catch {};
1332+
}
1333+
}.format,
1334+
&args,
1335+
);
1336+
}
1337+
1338+
fn bufPrintLogMessageTypeErased(
1339+
buffer: []u8,
1340+
message_type: types.MessageType,
1341+
format_fn: *const fn (std.io.AnyWriter, opaque_params: *const anyopaque) void,
1342+
opaque_params: *const anyopaque,
1343+
) []u8 {
1344+
std.debug.assert(buffer.len >= minimum_logging_buffer_size);
1345+
const json_message_suffix: []const u8 = "\"}}";
1346+
var fbs = std.io.fixedBufferStream(buffer[0 .. buffer.len - json_message_suffix.len]);
1347+
1348+
const writer = fbs.writer();
1349+
writer.print(
1350+
\\{{"jsonrpc":"2.0","method":"window/logMessage","params":{{"type":{},"message":"
1351+
, .{std.json.fmt(message_type, .{})}) catch unreachable;
1352+
1353+
const json_writer: std.io.Writer(*std.io.FixedBufferStream([]u8), error{NoSpaceLeft}, jsonWrite) = .{
1354+
.context = &fbs,
1355+
};
1356+
1357+
format_fn(json_writer.any(), opaque_params);
1358+
1359+
fbs.buffer = buffer;
1360+
fbs.writer().writeAll(json_message_suffix) catch unreachable;
1361+
1362+
return fbs.getWritten();
1363+
}
1364+
1365+
fn jsonWrite(fbs: *std.io.FixedBufferStream([]u8), bytes: []const u8) error{NoSpaceLeft}!usize {
1366+
var write_cursor: usize = 0;
1367+
var i: usize = 0;
1368+
while (i < bytes.len) : (i += 1) {
1369+
switch (bytes[i]) {
1370+
0x20...0x21, 0x23...0x5B, 0x5D...0xFF => {},
1371+
0x00...0x1F, '\\', '\"' => {
1372+
try fbsWriteOrEllipses(fbs, bytes[write_cursor..i]);
1373+
1374+
// either write an escape code in its entirety or don't at all
1375+
const pos = fbs.pos;
1376+
errdefer fbs.pos = pos;
1377+
1378+
const writer = fbs.writer();
1379+
1380+
switch (bytes[i]) {
1381+
'\\' => try writer.writeAll("\\\\"),
1382+
'\"' => try writer.writeAll("\\\""),
1383+
0x08 => try writer.writeAll("\\b"),
1384+
0x0C => try writer.writeAll("\\f"),
1385+
'\n' => try writer.writeAll("\\n"),
1386+
'\r' => try writer.writeAll("\\r"),
1387+
'\t' => try writer.writeAll("\\t"),
1388+
else => {
1389+
try fbs.writer().writeAll("\\u");
1390+
try std.fmt.formatIntValue(bytes[i], "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, fbs.writer());
1391+
},
1392+
}
1393+
1394+
write_cursor = i + 1;
1395+
},
1396+
}
1397+
}
1398+
1399+
try fbsWriteOrEllipses(fbs, bytes[write_cursor..]);
1400+
return bytes.len;
1401+
}
1402+
1403+
fn fbsWriteOrEllipses(
1404+
fbs: *std.io.FixedBufferStream([]u8),
1405+
bytes: []const u8,
1406+
) error{NoSpaceLeft}!void {
1407+
const ellipses: []const u8 = "...";
1408+
1409+
const pos_before_write = fbs.pos;
1410+
const amt = fbs.write(bytes) catch 0;
1411+
if (amt == bytes.len) return;
1412+
1413+
// try to move the buffer position back so that we have space for the ellipses
1414+
fbs.pos = @max(
1415+
pos_before_write, // make sure that we don't backtrack beyond an escape code
1416+
@min(fbs.pos, fbs.buffer.len - ellipses.len),
1417+
);
1418+
if (fbs.buffer.len - fbs.pos >= ellipses.len) {
1419+
fbs.writer().writeAll(ellipses) catch unreachable;
1420+
}
1421+
return error.NoSpaceLeft;
1422+
}
1423+
1424+
test bufPrintLogMessage {
1425+
var buffer: [1024]u8 = undefined;
1426+
const json_message = bufPrintLogMessage(
1427+
&buffer,
1428+
.Warning,
1429+
"Hello {s} '\\foo{s}bar'",
1430+
.{ "World", "\"" },
1431+
);
1432+
1433+
try std.testing.expectEqualStrings(
1434+
\\{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":2,"message":"Hello World '\\foo\"bar'"}}
1435+
, json_message);
1436+
}
1437+
1438+
test "bufPrintLogMessage - avoid buffer overflow" {
1439+
var buffer: [128]u8 = undefined;
1440+
const json_message = bufPrintLogMessage(
1441+
&buffer,
1442+
@enumFromInt(42),
1443+
"01234567890123456789012345678901234567890123456789",
1444+
.{},
1445+
);
1446+
1447+
try std.testing.expectEqualStrings(
1448+
\\{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":42,"message":"012345678901234567890123456789012345678901234..."}}
1449+
, json_message);
1450+
}
1451+
1452+
test "bufPrintLogMessage - avoid buffer overflow with escape codes" {
1453+
var buffer: [128]u8 = undefined;
1454+
const json_message = bufPrintLogMessage(
1455+
&buffer,
1456+
@enumFromInt(42),
1457+
"\x00" ** 128,
1458+
.{},
1459+
);
1460+
1461+
try std.testing.expectEqualStrings(
1462+
\\{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":42,"message":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"}}
1463+
, json_message);
1464+
}
1465+
13121466
pub const MethodWithParams = struct {
13131467
method: []const u8,
13141468
/// The `std.json.Value` can only be `.null`, `.array` or `.object`.

0 commit comments

Comments
 (0)