Skip to content

Commit ee0559d

Browse files
committed
Add filesystem func to transform a path to a URI
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, but after that became held up for unrelated reasons and another PR appeared that would benefit from this utility, I've split out this functionality so it can be used before the stacktrace printing PR is resolved.
1 parent 7e809b0 commit ee0559d

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

base/path.jl

+50
Original file line numberDiff line numberDiff line change
@@ -613,3 +613,53 @@ 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))
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))
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+
string("file://", encode_uri_component(gethostname()), '/', encode_uri_component(localpath))
662+
end
663+
end
664+
665+
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 = gethostname()
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)