Something went wrong. Try again.
Open a URI in the user's chosen application from Zig: the desktop portal on Linux, ShellExecuteExW on Windows, Launch Services on macOS.
Something went wrong. Try again.
7.6 kB · 159 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The checks every open request passes before it reaches the operating//! system, and the decoding of a `file:` URI into a local path. Neither needs//! a full URI parser: an open request only has to know the scheme, and a//! `file:` URI only has to yield its path.
const std = @import("std");
pub const Error = error{ InvalidUri, SchemeNotAllowed,};
/// Return the scheme of `uri` -- the text before the first `:` -- after/// checking that the URI is something it is safe to hand to an opener:////// - The scheme follows RFC 3986: a letter, then letters, digits, `+`, `-`/// or `.`./// - The scheme is at least two characters long. A single letter is a/// Windows drive (`C:\x\evil.exe`), and `ShellExecuteExW` would run it;/// see docs/adr/0001./// - The text is UTF-8 with no control characters, spaces or DEL, none of/// which may appear in a URI and any of which could make the opener read/// something other than what the caller checked./// - When `allowed` is not null the scheme is one of them, compared without/// regard to case as RFC 3986 requires.pub fn check(uri: []const u8, allowed: ?[]const []const u8) Error![]const u8 { if (!std.unicode.utf8ValidateSlice(uri)) return error.InvalidUri; for (uri) |c| if (c <= ' ' or c == 0x7f) return error.InvalidUri;
const colon = std.mem.findScalar(u8, uri, ':') orelse return error.InvalidUri; const scheme = uri[0..colon]; if (scheme.len == 0 or !std.ascii.isAlphabetic(scheme[0])) return error.InvalidUri; for (scheme[1..]) |c| switch (c) { 'a'...'z', 'A'...'Z', '0'...'9', '+', '-', '.' => {}, else => return error.InvalidUri, }; if (scheme.len < 2) return error.SchemeNotAllowed;
if (allowed) |schemes| { for (schemes) |name| { if (std.ascii.eqlIgnoreCase(name, scheme)) return scheme; } return error.SchemeNotAllowed; } return scheme;}
/// Whether `scheme`, as returned by `check`, is `file`.pub fn isFile(scheme: []const u8) bool { return std.ascii.eqlIgnoreCase(scheme, "file");}
/// Decode the path of a `file:` URI into `buffer`, returning it NUL/// terminated so it can be handed straight to the kernel.////// Accepted are `file:///path`, `file://localhost/path` and RFC 8089's/// `file:/path`; a file on another host cannot be opened locally, so any/// other authority is `error.InvalidUri`, as is a query. A fragment is/// dropped, because a file is opened by descriptor and a descriptor has/// nowhere to carry one. Percent-escapes are decoded, and an escape of NUL/// is refused rather than silently cutting the path short.pub fn filePath(uri: []const u8, buffer: []u8) Error![:0]const u8 { const colon = std.mem.findScalar(u8, uri, ':') orelse return error.InvalidUri; var rest = uri[colon + 1 ..]; if (std.mem.findScalar(u8, rest, '#')) |hash| rest = rest[0..hash]; if (std.mem.findScalar(u8, rest, '?') != null) return error.InvalidUri;
if (std.mem.startsWith(u8, rest, "//")) { const after = rest[2..]; const slash = std.mem.findScalar(u8, after, '/') orelse return error.InvalidUri; const host = after[0..slash]; if (host.len != 0 and !std.ascii.eqlIgnoreCase(host, "localhost")) return error.InvalidUri; rest = after[slash..]; } if (rest.len == 0 or rest[0] != '/') return error.InvalidUri;
var len: usize = 0; var i: usize = 0; while (i < rest.len) : (len += 1) { if (len + 1 >= buffer.len) return error.InvalidUri; if (rest[i] == '%') { if (i + 2 >= rest.len) return error.InvalidUri; const hi = std.fmt.charToDigit(rest[i + 1], 16) catch return error.InvalidUri; const lo = std.fmt.charToDigit(rest[i + 2], 16) catch return error.InvalidUri; const byte = hi * 16 + lo; if (byte == 0) return error.InvalidUri; buffer[len] = byte; i += 3; } else { buffer[len] = rest[i]; i += 1; } } buffer[len] = 0; return buffer[0..len :0];}
const testing = std.testing;
test "a scheme is returned" { try testing.expectEqualStrings("https", try check("https://example.com/", null)); try testing.expectEqualStrings("mailto", try check("mailto:someone@example.com", null)); try testing.expectEqualStrings("x-my.app+1", try check("x-my.app+1:thing", null));}
test "a drive letter is never a scheme" { try testing.expectError(error.SchemeNotAllowed, check("C:\\Windows\\System32\\calc.exe", null)); try testing.expectError(error.SchemeNotAllowed, check("c:/x", &.{"c"}));}
test "malformed URIs are refused" { try testing.expectError(error.InvalidUri, check("", null)); try testing.expectError(error.InvalidUri, check("example.com", null)); try testing.expectError(error.InvalidUri, check(":nothing", null)); try testing.expectError(error.InvalidUri, check("1http://x", null)); try testing.expectError(error.InvalidUri, check("ht tp://x", null)); try testing.expectError(error.InvalidUri, check("https://x/a b", null)); try testing.expectError(error.InvalidUri, check("https://x/\n", null)); try testing.expectError(error.InvalidUri, check("https://x/\x00", null)); try testing.expectError(error.InvalidUri, check("https://x/\x7f", null)); try testing.expectError(error.InvalidUri, check("https://x/\xff", null));}
test "an IRI with UTF-8 in it is allowed" { try testing.expectEqualStrings("https", try check("https://example.com/caf\u{e9}", null));}
test "the allowlist is compared without case" { const web: []const []const u8 = &.{ "http", "https", "mailto" }; try testing.expectEqualStrings("HTTPS", try check("HTTPS://example.com", web)); try testing.expectError(error.SchemeNotAllowed, check("file:///etc/passwd", web)); try testing.expectError(error.SchemeNotAllowed, check("https://x", &.{}));}
test "file paths are decoded" { var buffer: [64]u8 = undefined; try testing.expectEqualStrings("/tmp/a b", try filePath("file:///tmp/a%20b", &buffer)); try testing.expectEqualStrings("/tmp/x", try filePath("file://localhost/tmp/x", &buffer)); try testing.expectEqualStrings("/tmp/x", try filePath("FILE://LocalHost/tmp/x", &buffer)); try testing.expectEqualStrings("/tmp/x", try filePath("file:/tmp/x", &buffer)); try testing.expectEqualStrings("/tmp/x.html", try filePath("file:///tmp/x.html#part", &buffer)); try testing.expectEqualStrings("/tmp/caf\u{e9}", try filePath("file:///tmp/caf%C3%a9", &buffer)); try testing.expectEqualStrings("/", try filePath("file:///", &buffer));}
test "file URIs that cannot name a local file are refused" { var buffer: [16]u8 = undefined; try testing.expectError(error.InvalidUri, filePath("file://server/share/x", &buffer)); try testing.expectError(error.InvalidUri, filePath("file://localhost", &buffer)); try testing.expectError(error.InvalidUri, filePath("file:relative", &buffer)); try testing.expectError(error.InvalidUri, filePath("file:///x?y=1", &buffer)); try testing.expectError(error.InvalidUri, filePath("file:///x%00y", &buffer)); try testing.expectError(error.InvalidUri, filePath("file:///x%2", &buffer)); try testing.expectError(error.InvalidUri, filePath("file:///x%zz", &buffer)); try testing.expectError(error.InvalidUri, filePath("file:///a/very/long/path/indeed", &buffer));}