From e0655a7f75973dbc9013458587dac0c4e12ce66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 8 May 2025 22:36:37 -0400 Subject: [PATCH] Move url parsing helper to os/hostname Also adds a test to verify that the function is working as intended. --- src/os/hostname.zig | 154 ++++++++++++++++++++++++++++++++++ src/termio/stream_handler.zig | 78 +---------------- 2 files changed, 155 insertions(+), 77 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 22f29ceff..eb6c7052c 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -6,6 +6,91 @@ pub const HostnameParsingError = error{ NoSpaceLeft, }; +fn isUriPathSeparator(c: u8) bool { + return switch (c) { + '?', '#' => true, + else => false, + }; +} + +fn isValidMacAddress(mac_address: []const u8) bool { + // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. + if (mac_address.len != 17) { + return false; + } + + for (0..mac_address.len) |i| { + const c = mac_address[i]; + + if ((i + 1) % 3 == 0) { + if (c != ':') { + return false; + } + } else { + if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { + return false; + } + } + } + + return true; +} + +/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and +/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS +/// the url passed to this function might have a mac address as its hostname and parses it +/// correctly. +pub fn parseUrl(url: []const u8) !std.Uri { + return std.Uri.parse(url) catch |e| { + // It's possible this is a mac address on macOS where the last 2 characters in the + // address are non-digits, e.g. 'ff', and thus an invalid port. + // + // Example: file://12:34:56:78:90:12/path/to/file + if (e != error.InvalidPort) return e; + + const scheme, const url_without_scheme = url: { + if (std.mem.startsWith(u8, url, "file://")) break :url .{ "file", url[7..] }; + if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url .{ + "kitty-shell-cwd", + url[18..], + }; + + return error.UnsupportedScheme; + }; + + // The first '/' after the scheme marks the end of the hostname. If the first '/' + // following the end of the scheme is not at the right position this is not a + // valid mac address. + if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17 and + url_without_scheme.len != 17) + { + return error.HostnameIsNotMacAddress; + } + + // At this point we may have a mac address as the hostname. + const mac_address = url_without_scheme[0..17]; + + if (!isValidMacAddress(mac_address)) { + return error.HostnameIsNotMacAddress; + } + + var uri_path_end_idx: usize = 17; + while (uri_path_end_idx < url_without_scheme.len and + !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) + { + uri_path_end_idx += 1; + } + + // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI + // spec. + return .{ + .scheme = scheme, + .host = .{ .percent_encoded = mac_address }, + .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, + }; + }; +} + /// Print the hostname from a file URI into a buffer. pub fn bufPrintHostnameFromFileUri( buf: []u8, @@ -70,6 +155,67 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { return std.mem.eql(u8, hostname, ourHostname); } +test parseUrl { + // 1. Typical hostnames. + + var uri = try parseUrl("file://personal.computer/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + // 2. Hostnames that are mac addresses. + + // Numerical mac addresses. + + uri = try parseUrl("file://12:34:56:78:90:12/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + // Alphabetical mac addresses. + + uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); +} + +test "parseUrl succeeds even if path component is missing" { + const uri = try parseUrl("file://12:34:56:78:90:ab"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded); + try std.testing.expect(uri.path.isEmpty()); + try std.testing.expect(uri.port == null); +} + test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { const uri = try std.Uri.parse("file://localhost/"); @@ -86,6 +232,14 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); } +test "bufPrintHostnameFromFileUri succeeds with hostname as mac address with the last component as ascii" { + const uri = try parseUrl("file://12:34:56:78:90:ab"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + try std.testing.expectEqualStrings("12:34:56:78:90:ab", actual); +} + test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" { const uri = try std.Uri.parse("file://12:34:56:78:90:05"); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index d57bdb1ac..a238c2a59 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1041,82 +1041,6 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } - fn isUriPathSeparator(c: u8) bool { - return switch (c) { - '?', '#' => true, - else => false, - }; - } - - fn isValidMacAddress(mac_address: []const u8) bool { - // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. - if (mac_address.len != 17) { - return false; - } - - for (0..mac_address.len) |i| { - const c = mac_address[i]; - - if ((i + 1) % 3 == 0) { - if (c != ':') { - return false; - } - } else { - if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { - return false; - } - } - } - - return true; - } - - fn parseUrl(url: []const u8) !std.Uri { - return std.Uri.parse(url) catch |e| { - // It's possible this is a mac address on macOS where the last 2 characters in the - // address are non-digits, e.g. 'ff', and thus an invalid port. - // - // Example: file://12:34:56:78:90:12/path/to/file - if (e != error.InvalidPort) return e; - - const url_without_scheme = url: { - if (std.mem.startsWith(u8, url, "file://")) break :url url[7..]; - if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url url[18..]; - - return error.UnsupportedScheme; - }; - - // The first '/' after the scheme marks the end of the hostname. If the first '/' - // following the end of the scheme is not at the right position this is not a - // valid mac address. - if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) { - return error.HostnameIsNotMacAddress; - } - - // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..17]; - - if (!isValidMacAddress(mac_address)) { - return error.HostnameIsNotMacAddress; - } - - var uri_path_end_idx: usize = 17; - while (uri_path_end_idx < url_without_scheme.len and - !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) - { - uri_path_end_idx += 1; - } - - // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI - // spec. - return .{ - .scheme = "file", - .host = .{ .percent_encoded = mac_address }, - .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, - }; - }; - } - pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any @@ -1145,7 +1069,7 @@ pub const StreamHandler = struct { return; } - const uri: std.Uri = parseUrl(url) catch |e| { + const uri: std.Uri = internal_os.hostname.parseUrl(url) catch |e| { log.warn("invalid url in OSC 7: {}", .{e}); return; };