schema: reject backslash escapes in quoted values

Explicitly reject backslash-escaped quotes with a clear error
message, suggesting alternative quote styles or triple quotes.
Bare backslashes (e.g. in Windows paths) remain supported.
This commit is contained in:
Adrià Arrufat
2026-05-24 11:35:07 +02:00
parent 4a58a9043e
commit aed5bbf1b6
2 changed files with 26 additions and 2 deletions

View File

@@ -600,6 +600,7 @@ fn printSlashParseError(self: *Agent, err: Schema.ParseError, name: []const u8)
error.UnknownField => "unknown field (typo?)",
error.PositionalNotAllowed => "positional only works for tools with one required field. Use key=value",
error.UnterminatedQuote => "unterminated quote",
error.UnsupportedEscape => "backslash escapes aren't supported in quoted values; use the other quote style or `'''…'''`",
error.OutOfMemory => return self.terminal.printError("out of memory", .{}),
};
self.terminal.printError("{s}: {s}. Try /help {s}.", .{ name, reason, name });

View File

@@ -70,6 +70,7 @@ pub const ParseError = error{
MalformedKv,
PositionalNotAllowed,
UnterminatedQuote,
UnsupportedEscape,
OutOfMemory,
};
@@ -423,8 +424,15 @@ fn tokenize(arena: std.mem.Allocator, input: []const u8) ParseError![][]const u8
const close = std.mem.indexOfPos(u8, input, i + 3, triple_delim) orelse return error.UnterminatedQuote;
i = close + 2;
} else {
const close = std.mem.indexOfScalarPos(u8, input, i + 1, ch) orelse return error.UnterminatedQuote;
i = close;
// Scan for the closer. `\<quote>` is rejected rather
// than decoded — choose the other quote style or a
// triple-quoted block instead.
var j = i + 1;
while (j < input.len) : (j += 1) {
if (input[j] == '\\' and j + 1 < input.len and input[j + 1] == ch) return error.UnsupportedEscape;
if (input[j] == ch) break;
} else return error.UnterminatedQuote;
i = j;
}
}
}
@@ -767,6 +775,21 @@ test "tokenize: inline triple quotes with spaces" {
try testing.expectString("value=\"\"\"foo bar\"\"\"", tokens[1]);
}
test "tokenize: rejects backslash-escaped quote inside same-style quote" {
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
defer arena.deinit();
try testing.expectError(error.UnsupportedEscape, tokenize(arena.allocator(), "value=\"hello \\\"world\\\"\""));
try testing.expectError(error.UnsupportedEscape, tokenize(arena.allocator(), "value='it\\'s'"));
}
test "tokenize: bare backslash inside quotes is allowed (e.g. Windows paths)" {
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
defer arena.deinit();
const tokens = try tokenize(arena.allocator(), "value='C:\\Users\\bob'");
try testing.expectEqual(@as(usize, 1), tokens.len);
try testing.expectString("value='C:\\Users\\bob'", tokens[0]);
}
test "hasUnclosedTripleQuote" {
try testing.expect(!hasUnclosedTripleQuote(""));
try testing.expect(!hasUnclosedTripleQuote("/goto https://x"));