diff --git a/src/os/hostname.zig b/src/os/hostname.zig index a75ca1cbb..283ece8d9 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,148 +1,11 @@ const std = @import("std"); -const builtin = @import("builtin"); const posix = std.posix; -pub const HostnameParsingError = error{ - NoHostnameInUri, - NoSpaceLeft, -}; - pub const UrlParsingError = std.Uri.ParseError || error{ HostnameIsNotMacAddress, NoSchemeProvided, }; -const mac_address_length = 17; - -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 (mac_address, 0..) |c, 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) UrlParsingError!std.Uri { - return std.Uri.parse(url) catch |e| { - // The mac-address-as-hostname issue is specific to macOS so we just return an error if we - // hit it on other platforms. - if (comptime builtin.os.tag != .macos) return 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_start = std.mem.indexOf(u8, url, "://") orelse { - return error.NoSchemeProvided; - }; - const scheme = url[0..url_without_scheme_start]; - const url_without_scheme = url[url_without_scheme_start + 3 ..]; - - // 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 (url_without_scheme.len != mac_address_length and - std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length) - { - return error.HostnameIsNotMacAddress; - } - - // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..mac_address_length]; - - if (!isValidMacAddress(mac_address)) { - return error.HostnameIsNotMacAddress; - } - - var uri_path_end_idx: usize = mac_address_length; - 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[mac_address_length..uri_path_end_idx], - }, - }; - }; -} - -/// Print the hostname from a file URI into a buffer. -pub fn bufPrintHostnameFromFileUri( - buf: []u8, - uri: std.Uri, -) HostnameParsingError![]const u8 { - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return error.NoHostnameInUri; - const host: []const u8 = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a random mac address, e.g. '12:34:56:78:90:ab'. - // The URI will be parsed as if the last set of digits is a port number, so - // we need to make sure that part is included when it's set. - - // We're only interested in special port handling when the current hostname is a - // partial MAC address that's potentially missing the last component. - // If that's not the case we just return the plain URI hostname directly. - // NOTE: This implementation is not sufficient to verify a valid mac address, but - // it's probably sufficient for this specific purpose. - if (host.len != 14 or std.mem.count(u8, host, ":") != 4) return host; - - // If we don't have a port then we can return the hostname as-is because - // it's not a partial MAC-address. - const port = uri.port orelse return host; - - // If the port is not a 1 or 2-digit number we're not looking at a partial - // MAC-address, and instead just a regular port so we return the plain - // URI hostname. - if (port > 99) return host; - - var fbs = std.io.fixedBufferStream(buf); - try std.fmt.format( - fbs.writer(), - // Make sure "port" is always 2 digits, prefixed with a 0 when "port" is a 1-digit number. - "{s}:{d:0>2}", - .{ host, port }, - ); - - return fbs.getWritten(); -} - pub const LocalHostnameValidationError = error{ PermissionDenied, Unexpected, @@ -151,7 +14,7 @@ pub const LocalHostnameValidationError = error{ /// Checks if a hostname is local to the current machine. This matches /// both "localhost" and the current hostname of the machine (as returned /// by `gethostname`). -pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { +pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. if (std.mem.eql(u8, "localhost", hostname)) return true; @@ -161,185 +24,19 @@ 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); - - // 3. Hostnames that are mac addresses with no path. - - // Numerical mac addresses. - - uri = try parseUrl("file://12:34:56:78:90:12"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12"); - - 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("", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - // Alphabetical mac addresses. - - uri = try parseUrl("file://ab:cd:ef:ab:cd:ef"); - - 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("", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef"); - - 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("", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); +test "isLocal returns true when provided hostname is localhost" { + try std.testing.expect(try isLocal("localhost")); } -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/"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("localhost", actual); -} - -test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { - const uri = try std.Uri.parse("file://12:34:56:78:90:12"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - 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"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:05", actual); -} - -test "bufPrintHostnameFromFileUri returns only hostname when there is a port component in the URI" { - // First: try with a non-2-digit port, to test general port handling. - const four_port_uri = try std.Uri.parse("file://has-a-port:1234"); - - var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; - const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri); - try std.testing.expectEqualStrings("has-a-port", four_port_actual); - - // Second: try with a 2-digit port to test mac-address handling. - const two_port_uri = try std.Uri.parse("file://has-a-port:12"); - - var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; - const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri); - try std.testing.expectEqualStrings("has-a-port", two_port_actual); - - // Third: try with a mac-address that has a port-component added to it to test mac-address handling. - const mac_with_port_uri = try std.Uri.parse("file://12:34:56:78:90:12:1234"); - - var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; - const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); -} - -test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is missing from uri" { - const uri = try std.Uri.parse("file:///"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual); -} - -test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer has insufficient size" { - const uri = try std.Uri.parse("file://12:34:56:78:90:12/"); - - var buf: [5]u8 = undefined; - const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual); -} - -test "isLocalHostname returns true when provided hostname is localhost" { - try std.testing.expect(try isLocalHostname("localhost")); -} - -test "isLocalHostname returns true when hostname is local" { +test "isLocal returns true when hostname is local" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const localHostname = try posix.gethostname(&buf); - try std.testing.expect(try isLocalHostname(localHostname)); + try std.testing.expect(try isLocal(localHostname)); } -test "isLocalHostname returns false when hostname is not local" { +test "isLocal returns false when hostname is not local" { try std.testing.expectEqual( false, - try isLocalHostname("not-the-local-hostname"), + try isLocal("not-the-local-hostname"), ); } diff --git a/src/os/main.zig b/src/os/main.zig index af851f673..2d269e412 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -29,6 +29,7 @@ pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); pub const macos = @import("macos.zig"); pub const shell = @import("shell.zig"); +pub const uri = @import("uri.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); @@ -67,6 +68,7 @@ pub const getKernelInfo = kernel_info.getKernelInfo; test { _ = i18n; _ = path; + _ = uri; if (comptime builtin.os.tag == .linux) { _ = kernel_info; diff --git a/src/os/uri.zig b/src/os/uri.zig new file mode 100644 index 000000000..3d674870c --- /dev/null +++ b/src/os/uri.zig @@ -0,0 +1,204 @@ +const std = @import("std"); + +pub const ParseOptions = struct { + /// Parse MAC addresses in the host component. + /// + /// This is useful when the "Private Wi-Fi address" is enabled on macOS, + /// which sets the hostname to a rotating MAC address (12:34:56:ab:cd:ef). + mac_address: bool = false, + + /// Return the full, raw, unencoded path string. Any query and fragment + /// values will be return as part of the path instead of as distinct + /// fields. + raw_path: bool = false, +}; + +pub const ParseError = std.Uri.ParseError || error{InvalidMacAddress}; + +/// Parses a URI from the given string. +/// +/// This extends std.Uri.parse with some additional ParseOptions. +pub fn parse(text: []const u8, options: ParseOptions) ParseError!std.Uri { + var uri = std.Uri.parse(text) catch |err| uri: { + // We can attempt to re-parse the text as a URI that has a MAC address + // in its host field (which tripped up std.Uri.parse's port parsing): + // + // file://12:34:56:78:90:aa/path/to/file + // ^^ InvalidPort + // + if (err != error.InvalidPort or !options.mac_address) return err; + + // We can assume that the initial Uri.parse already validated the + // scheme, so we only need to find its bounds within the string. + const scheme_end = std.mem.indexOf(u8, text, "://") orelse { + return error.InvalidFormat; + }; + const scheme = text[0..scheme_end]; + + // We similarly find the bounds of the host component by looking + // for the first slash (/) after the scheme. This is all we need + // for this case because the resulting slice can be unambiguously + // determined to be a MAC address (or not). + const host_start = scheme_end + "://".len; + const host_end = std.mem.indexOfScalarPos(u8, text, host_start, '/') orelse text.len; + const mac_address = text[host_start..host_end]; + if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress; + + // Parse the rest of the text (starting with the path component) as a + // partial URI and then add our MAC address as its host component. + var uri = try std.Uri.parseAfterScheme(scheme, text[host_end..]); + uri.host = .{ .percent_encoded = mac_address }; + break :uri uri; + }; + + // When MAC address parsing is enabled, we need to handle the case where + // std.Uri.parse parsed the address's last octet as a numeric port number. + // We use a few heuristics to identify this case (14 characters, 4 colons) + // and then "repair" the result by reassign the .host component to the full + // MAC address and clearing the .port component. + // + // 12:34:56:78:90:99 -> [12:34:56:78:90, 99] -> 12:34:56:78:90:99 + // (original host) (parsed host + port) (restored host) + // + if (options.mac_address and uri.host != null) mac: { + const host = uri.host.?.percent_encoded; + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) break :mac; + + const port = uri.port orelse break :mac; + if (port > 99) break :mac; + + // std.Uri.parse returns slices pointing into the original text string. + const host_start = @intFromPtr(host.ptr) - @intFromPtr(text.ptr); + const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr); + const mac_address = text[host_start..path_start]; + if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress; + + uri.host = .{ .percent_encoded = mac_address }; + uri.port = null; + } + + // When the raw_path option is active, return everything after the authority + // (host) in the .path component, including any query and fragment values. + if (options.raw_path) { + // std.Uri.parse returns slices pointing into the original text string. + const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr); + uri.path = .{ .raw = text[path_start..] }; + uri.query = null; + uri.fragment = null; + } + + return uri; +} + +test "parse: mac_address" { + const testing = @import("std").testing; + + // Numeric MAC address without a port + const uri1 = try parse("file://00:12:34:56:78:90/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri1.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri1.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri1.path.percent_encoded); + try testing.expectEqual(null, uri1.port); + + // Numeric MAC address with a port + const uri2 = try parse("file://00:12:34:56:78:90:999/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri2.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri2.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri2.path.percent_encoded); + try testing.expectEqual(999, uri2.port); + + // Alphabetic MAC address without a port + const uri3 = try parse("file://ab:cd:ef:ab:cd:ef/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri3.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri3.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri3.path.percent_encoded); + try testing.expectEqual(null, uri3.port); + + // Alphabetic MAC address with a port + const uri4 = try parse("file://ab:cd:ef:ab:cd:ef:999/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri4.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri4.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri4.path.percent_encoded); + try testing.expectEqual(999, uri4.port); + + // Numeric MAC address without a path component + const uri5 = try parse("file://00:12:34:56:78:90", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri5.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri5.host.?.percent_encoded); + try testing.expect(uri5.path.isEmpty()); + + // Alphabetic MAC address without a path component + const uri6 = try parse("file://ab:cd:ef:ab:cd:ef", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri6.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri6.host.?.percent_encoded); + try testing.expect(uri6.path.isEmpty()); + + // Invalid MAC addresses + try testing.expectError(error.InvalidMacAddress, parse( + "file://zz:zz:zz:zz:zz:00/path", + .{ .mac_address = true }, + )); + try testing.expectError(error.InvalidMacAddress, parse( + "file://zz:zz:zz:zz:zz:zz/path", + .{ .mac_address = true }, + )); +} + +test "parse: raw_path" { + const testing = @import("std").testing; + + const text = "file://localhost/path??#fragment"; + var buf: [256]u8 = undefined; + + const uri1 = try parse(text, .{ .raw_path = false }); + try testing.expectEqualStrings("file", uri1.scheme); + try testing.expectEqualStrings("localhost", uri1.host.?.percent_encoded); + try testing.expectEqualStrings("/path", try uri1.path.toRaw(&buf)); + try testing.expectEqualStrings("?", uri1.query.?.percent_encoded); + try testing.expectEqualStrings("fragment", uri1.fragment.?.percent_encoded); + + const uri2 = try parse(text, .{ .raw_path = true }); + try testing.expectEqualStrings("file", uri2.scheme); + try testing.expectEqualStrings("localhost", uri2.host.?.percent_encoded); + try testing.expectEqualStrings("/path??#fragment", try uri2.path.toRaw(&buf)); + try testing.expectEqual(null, uri2.query); + try testing.expectEqual(null, uri2.fragment); + + const uri3 = try parse("file://localhost", .{ .raw_path = true }); + try testing.expectEqualStrings("file", uri3.scheme); + try testing.expectEqualStrings("localhost", uri3.host.?.percent_encoded); + try testing.expect(uri3.path.isEmpty()); + try testing.expectEqual(null, uri3.query); + try testing.expectEqual(null, uri3.fragment); +} + +/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef. +fn isValidMacAddress(s: []const u8) bool { + if (s.len != 17) return false; + + for (s, 0..) |c, i| { + if (i % 3 == 2) { + if (c != ':') return false; + } else { + switch (c) { + '0'...'9', 'A'...'F', 'a'...'f' => {}, + else => return false, + } + } + } + + return true; +} + +test isValidMacAddress { + const testing = @import("std").testing; + + try testing.expect(isValidMacAddress("01:23:45:67:89:Aa")); + try testing.expect(isValidMacAddress("Aa:Bb:Cc:Dd:Ee:Ff")); + + try testing.expect(!isValidMacAddress("")); + try testing.expect(!isValidMacAddress("00:23:45")); + try testing.expect(!isValidMacAddress("00:23:45:Xx:Yy:Zz")); + try testing.expect(!isValidMacAddress("01-23-45-67-89-Aa")); + try testing.expect(!isValidMacAddress("01:23:45:67:89:Aa:Bb")); +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 06ff29809..db2cf11a6 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1089,7 +1089,13 @@ pub const StreamHandler = struct { return; } - const uri: std.Uri = internal_os.hostname.parseUrl(url) catch |e| { + // Attempt to parse this file-style URI using options appropriate + // for this OSC 7 context (e.g. kitty-shell-cwd expects the full, + // unencoded path). + const uri: std.Uri = internal_os.uri.parse(url, .{ + .mac_address = comptime builtin.os.tag != .macos, + .raw_path = std.mem.startsWith(u8, url, "kitty-shell-cwd://"), + }) catch |e| { log.warn("invalid url in OSC 7: {}", .{e}); return; }; @@ -1097,26 +1103,18 @@ pub const StreamHandler = struct { if (!std.mem.eql(u8, "file", uri.scheme) and !std.mem.eql(u8, "kitty-shell-cwd", uri.scheme)) { - log.warn("OSC 7 scheme must be file, got: {s}", .{uri.scheme}); + log.warn("OSC 7 scheme must be file or kitty-shell-cwd, got: {s}", .{uri.scheme}); return; } - // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent - // the maximum since 2^16 - 1 = 65_535. - // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. - const PORT_NUMBER_MAX_DIGITS = 5; - // Make sure there is space for a max length hostname + the max number of digits. - var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - const hostname_from_uri = internal_os.hostname.bufPrintHostnameFromFileUri( - &host_and_port_buf, - uri, - ) catch |err| switch (err) { - error.NoHostnameInUri => { + var host_buffer: [std.Uri.host_name_max]u8 = undefined; + const host = uri.getHost(&host_buffer) catch |err| switch (err) { + error.UriMissingHost => { log.warn("OSC 7 uri must contain a hostname: {}", .{err}); return; }, - error.NoSpaceLeft => |e| { - log.warn("failed to get full hostname for OSC 7 validation: {}", .{e}); + error.UriHostTooLong => { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); return; }, }; @@ -1124,9 +1122,7 @@ pub const StreamHandler = struct { // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = internal_os.hostname.isLocalHostname( - hostname_from_uri, - ) catch |err| switch (err) { + const host_valid = internal_os.hostname.isLocal(host) catch |err| switch (err) { error.PermissionDenied, error.Unexpected, => { @@ -1135,43 +1131,16 @@ pub const StreamHandler = struct { }, }; if (!host_valid) { - log.warn("OSC 7 host must be local", .{}); + log.warn("OSC 7 host ({s}) must be local", .{host}); return; } - // We need to unescape the path. We first try to unescape onto - // the stack and fall back to heap allocation if we have to. - var path_buf: [1024]u8 = undefined; - const path, const heap = path: { - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const path = switch (uri.path) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // If the path doesn't have any escapes, we can use it directly. - if (std.mem.indexOfScalar(u8, path, '%') == null) - break :path .{ path, false }; - - // First try to stack-allocate - var stack_writer: std.Io.Writer = .fixed(&path_buf); - if (uri.path.formatRaw(&stack_writer)) |_| { - break :path .{ stack_writer.buffered(), false }; - } else |_| {} - - // Fall back to heap - var alloc_writer: std.Io.Writer.Allocating = .init(self.alloc); - if (uri.path.formatRaw(&alloc_writer.writer)) |_| { - break :path .{ alloc_writer.written(), true }; - } else |_| {} - - // Fall back to using it directly... - log.warn("failed to unescape OSC 7 path, using it directly path={s}", .{path}); - break :path .{ path, false }; - }; - defer if (heap) self.alloc.free(path); + // We need the raw path, which might require unescaping. We try to + // avoid making any heap allocations by using the stack first. + var arena_alloc: std.heap.ArenaAllocator = .init(self.alloc); + var stack_alloc = std.heap.stackFallback(1024, arena_alloc.allocator()); + defer arena_alloc.deinit(); + const path = try uri.path.toRawMaybeAlloc(stack_alloc.get()); log.debug("terminal pwd: {s}", .{path}); try self.terminal.setPwd(path);