Skip to content

Commit a06a801

Browse files
authored
Add filesystem func to transform a path to a URI (#55454)
In a few places across Base and the stdlib, we emit paths that we like people to be able to click on in their terminal and editor. Up to this point, we have relied on auto-filepath detection, but this does not allow for alternative link text, such as contracted paths. Doing so (via OSC 8 terminal links for example) requires filepath URI encoding. This functionality was previously part of a PR modifying stacktrace printing (#51816), but after that became held up for unrelated reasons and another PR appeared that would benefit from this utility (#55335), I've split out this functionality so it can be used before the stacktrace printing PR is resolved.
1 parent 2943833 commit a06a801

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

base/path.jl

+56
Original file line numberDiff line numberDiff line change
@@ -613,3 +613,59 @@ relpath(path::AbstractString, startpath::AbstractString) =
613613
for f in (:isdirpath, :splitdir, :splitdrive, :splitext, :normpath, :abspath)
614614
@eval $f(path::AbstractString) = $f(String(path))
615615
end
616+
617+
"""
618+
uripath(path::AbstractString)
619+
620+
Encode `path` as a URI as per [RFC8089: The "file" URI
621+
Scheme](https://www.rfc-editor.org/rfc/rfc8089), [RFC3986: Uniform Resource
622+
Identifier (URI): Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986), and
623+
the [Freedesktop File URI spec](https://www.freedesktop.org/wiki/Specifications/file-uri-spec/).
624+
625+
## Examples
626+
627+
```julia-repl
628+
julia> uripath("/home/user/example file.jl") # On a unix machine
629+
"file://<hostname>/home/user/example%20file.jl"
630+
631+
juila> uripath("C:\\Users\\user\\example file.jl") # On a windows machine
632+
"file:///C:/Users/user/example%20file.jl"
633+
```
634+
"""
635+
function uripath end
636+
637+
@static if Sys.iswindows()
638+
function uripath(path::String)
639+
percent_escape(s) = # RFC3986 Section 2.1
640+
'%' * join(map(b -> uppercase(string(b, base=16)), codeunits(s)), '%')
641+
encode_uri_component(s) = # RFC3986 Section 2.3
642+
replace(s, r"[^A-Za-z0-9\-_.~/]+" => percent_escape)
643+
path = abspath(path)
644+
if startswith(path, "\\\\") # UNC path, RFC8089 Appendix E.3
645+
unixpath = join(eachsplit(path, path_separator_re, keepempty=false), '/')
646+
string("file://", encode_uri_component(unixpath)) # RFC8089 Section 2
647+
else
648+
drive, localpath = splitdrive(path) # Assuming that non-UNC absolute paths on Windows always have a drive component
649+
unixpath = join(eachsplit(localpath, path_separator_re, keepempty=false), '/')
650+
encdrive = replace(encode_uri_component(drive), "%3A" => ':', "%7C" => '|') # RFC8089 Appendices D.2, E.2.1, and E.2.2
651+
string("file:///", encdrive, '/', encode_uri_component(unixpath)) # RFC8089 Section 2
652+
end
653+
end
654+
else
655+
function uripath(path::String)
656+
percent_escape(s) = # RFC3986 Section 2.1
657+
'%' * join(map(b -> uppercase(string(b, base=16)), codeunits(s)), '%')
658+
encode_uri_component(s) = # RFC3986 Section 2.3
659+
replace(s, r"[^A-Za-z0-9\-_.~/]+" => percent_escape)
660+
localpath = join(eachsplit(abspath(path), path_separator_re, keepempty=false), '/')
661+
host = if ispath("/proc/sys/fs/binfmt_misc/WSLInterop") # WSL sigil
662+
distro = get(ENV, "WSL_DISTRO_NAME", "") # See <https://patrickwu.space/wslconf/>
663+
"wsl\$/$distro" # See <https://github.com/microsoft/terminal/pull/14993> and <https://learn.microsoft.com/en-us/windows/wsl/filesystems>
664+
else
665+
gethostname() # Freedesktop File URI Spec, Hostnames section
666+
end
667+
string("file://", encode_uri_component(host), '/', encode_uri_component(localpath)) # RFC8089 Section 2
668+
end
669+
end
670+
671+
uripath(path::AbstractString) = uripath(String(path))

test/path.jl

+13
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,19 @@
311311
test_relpath()
312312
end
313313

314+
@testset "uripath" begin
315+
host = if Sys.iswindows() "" else gethostname() end
316+
sysdrive, uridrive = if Sys.iswindows() "C:\\", "C:/" else "/", "" end
317+
@test Base.Filesystem.uripath("$(sysdrive)some$(sep)file.txt") == "file://$host/$(uridrive)some/file.txt"
318+
@test Base.Filesystem.uripath("$(sysdrive)another$(sep)$(sep)folder$(sep)file.md") == "file://$host/$(uridrive)another/folder/file.md"
319+
@test Base.Filesystem.uripath("$(sysdrive)some file with ^odd% chars") == "file://$host/$(uridrive)some%20file%20with%20%5Eodd%25%20chars"
320+
@test Base.Filesystem.uripath("$(sysdrive)weird chars like @#&()[]{}") == "file://$host/$(uridrive)weird%20chars%20like%20%40%23%26%28%29%5B%5D%7B%7D"
321+
@test Base.Filesystem.uripath("$sysdrive") == "file://$host/$uridrive"
322+
@test Base.Filesystem.uripath(".") == Base.Filesystem.uripath(pwd())
323+
@test Base.Filesystem.uripath("$(sysdrive)unicode$(sep)Δεδομένα") == "file://$host/$(uridrive)unicode/%CE%94%CE%B5%CE%B4%CE%BF%CE%BC%CE%AD%CE%BD%CE%B1"
324+
@test Base.Filesystem.uripath("$(sysdrive)unicode$(sep)🧮🐛🔨") == "file://$host/$(uridrive)unicode/%F0%9F%A7%AE%F0%9F%90%9B%F0%9F%94%A8"
325+
end
326+
314327
if Sys.iswindows()
315328
@testset "issue #23646" begin
316329
@test lowercase(relpath("E:\\a\\b", "C:\\c")) == "e:\\a\\b"

0 commit comments

Comments
 (0)