windows: handle backslash paths in config value parsing
CommaSplitter treats backslash as an escape character, which breaks Windows paths like C:\Users\foo since \U is not a valid escape. On Windows, treat backslash as a literal character outside of quoted strings. Inside quotes, escape sequences still work as before. The platform behavior is controlled by a single comptime constant (escape_outside_quotes) so the logic lives in one place. Escape-specific tests are skipped on Windows with SkipZigTest, and Windows-specific tests are added separately. Also fix Theme.parseCLI to not mistake the colon in a Windows drive letter (C:\...) for a light/dark theme pair separator. Note: other places in the config parsing also use colon as a delimiter without accounting for Windows drive letters (command.zig prefix parsing, keybind parsing). Those are tracked separately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>pull/11782/head
parent
fa10237fb0
commit
909e733120
|
|
@ -13,8 +13,18 @@
|
|||
//!
|
||||
//! Quotes and escapes are not stripped or decoded, that must be handled as a
|
||||
//! separate step!
|
||||
//!
|
||||
//! On Windows, backslash is only treated as an escape character inside quoted
|
||||
//! strings. Outside quotes, backslash is a literal character (path separator).
|
||||
const CommaSplitter = @This();
|
||||
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Whether backslash acts as an escape character outside quoted strings.
|
||||
/// On Windows, backslash is the path separator so it is always literal
|
||||
/// outside quotes.
|
||||
const escape_outside_quotes = builtin.os.tag != .windows;
|
||||
|
||||
pub const Error = error{
|
||||
UnclosedQuote,
|
||||
UnfinishedEscape,
|
||||
|
|
@ -77,8 +87,11 @@ pub fn next(self: *CommaSplitter) Error!?[]const u8 {
|
|||
},
|
||||
'\\' => {
|
||||
self.index += 1;
|
||||
last = .normal;
|
||||
continue :loop .escape;
|
||||
if (comptime escape_outside_quotes) {
|
||||
last = .normal;
|
||||
continue :loop .escape;
|
||||
}
|
||||
continue :loop .normal;
|
||||
},
|
||||
else => {
|
||||
self.index += 1;
|
||||
|
|
@ -273,6 +286,7 @@ test "splitter 8" {
|
|||
}
|
||||
|
||||
test "splitter 9" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -281,6 +295,7 @@ test "splitter 9" {
|
|||
}
|
||||
|
||||
test "splitter 10" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -289,6 +304,7 @@ test "splitter 10" {
|
|||
}
|
||||
|
||||
test "splitter 11" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -297,6 +313,7 @@ test "splitter 11" {
|
|||
}
|
||||
|
||||
test "splitter 12" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -305,6 +322,7 @@ test "splitter 12" {
|
|||
}
|
||||
|
||||
test "splitter 13" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -313,6 +331,7 @@ test "splitter 13" {
|
|||
}
|
||||
|
||||
test "splitter 14" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -330,6 +349,7 @@ test "splitter 15" {
|
|||
}
|
||||
|
||||
test "splitter 16" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -338,6 +358,7 @@ test "splitter 16" {
|
|||
}
|
||||
|
||||
test "splitter 17" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -346,6 +367,7 @@ test "splitter 17" {
|
|||
}
|
||||
|
||||
test "splitter 18" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -415,6 +437,7 @@ test "splitter 24" {
|
|||
}
|
||||
|
||||
test "splitter 25" {
|
||||
if (comptime !escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
|
|
@ -422,3 +445,39 @@ test "splitter 25" {
|
|||
try testing.expectEqualStrings("a", (try s.next()).?);
|
||||
try testing.expectError(error.IllegalEscape, s.next());
|
||||
}
|
||||
|
||||
// Windows-specific tests: backslash is literal outside quotes.
|
||||
|
||||
test "splitter: windows paths" {
|
||||
if (comptime escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
var s: CommaSplitter = .init("light:C:\\Users\\foo\\theme,dark:C:\\Users\\bar\\theme");
|
||||
try testing.expectEqualStrings("light:C:\\Users\\foo\\theme", (try s.next()).?);
|
||||
try testing.expectEqualStrings("dark:C:\\Users\\bar\\theme", (try s.next()).?);
|
||||
try testing.expect(null == try s.next());
|
||||
}
|
||||
|
||||
test "splitter: backslash literal outside quotes on windows" {
|
||||
if (comptime escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
// Backslash followed by characters that would be escapes on Unix
|
||||
// are treated as literal on Windows outside quotes.
|
||||
var s: CommaSplitter = .init("\\n\\r\\t");
|
||||
try testing.expectEqualStrings("\\n\\r\\t", (try s.next()).?);
|
||||
try testing.expect(null == try s.next());
|
||||
}
|
||||
|
||||
test "splitter: backslash still escapes inside quotes on windows" {
|
||||
if (comptime escape_outside_quotes) return error.SkipZigTest;
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
// Inside quotes, backslash escapes work on all platforms.
|
||||
var s: CommaSplitter = .init("\"hello\\nworld\"");
|
||||
try testing.expectEqualStrings("\"hello\\nworld\"", (try s.next()).?);
|
||||
try testing.expect(null == try s.next());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9810,9 +9810,16 @@ pub const Theme = struct {
|
|||
// we're parsing a light/dark mode theme pair. Note that "=" isn't
|
||||
// actually valid for setting a light/dark mode pair but I anticipate
|
||||
// it'll be a common typo.
|
||||
//
|
||||
// On Windows, a colon at index 1 is a drive letter (e.g. C:\...)
|
||||
// and should not trigger light/dark pair parsing.
|
||||
const has_colon = if (comptime builtin.os.tag == .windows)
|
||||
if (std.mem.indexOf(u8, input, ":")) |idx| idx != 1 else false
|
||||
else
|
||||
std.mem.indexOf(u8, input, ":") != null;
|
||||
if (std.mem.indexOf(u8, input, ",") != null or
|
||||
std.mem.indexOf(u8, input, "=") != null or
|
||||
std.mem.indexOf(u8, input, ":") != null)
|
||||
has_colon)
|
||||
{
|
||||
self.* = try cli.args.parseAutoStruct(
|
||||
Theme,
|
||||
|
|
|
|||
Loading…
Reference in New Issue