lib.Uri: Fix __assemble(): No '://' for relative paths

Assembling a scheme-bearing form always writes '://' after the scheme, even
if the input has no authority part. For a schemeless relative path such as
'../../local/path', .full then becomes 'file://../../local/path', which
re-parses with '..' as the host and '/../local/path' as the path, both
entirely broken.

Fix __assemble() to write a bare ':' instead of '://' when the input has no
host and a relative path, so the assembled form re-parses back to the same
relative path: full of '../../local/path' is now 'file:../../local/path'.
Absolute paths and forms with an authority are unchanged.

The assembled 'file:../../local/path' form is a valid URI under the RFC
3986 generic grammar (scheme with a rootless path), while RFC 8089's file
URI ABNF admits only an empty or an absolute path. Uri thus trades RFC 8089
conformance for RFC 3986 compatibility: a relative path survives the
full-and-reparse cycle unchanged.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-08-31 12:03:57 +02:00
commit a1ae17c8cb
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
2 changed files with 23 additions and 1 deletions

View file

@ -18,7 +18,12 @@ class Uri:
if self.__p.path.startswith('~'):
return self.path
if scheme:
ret += f'{self.protocol}://'
if self.hostname is None and not self.path.startswith('/'):
# -- No authority and a relative path: '://' would
# re-parse the first path segment as a host
ret += f'{self.protocol}:'
else:
ret += f'{self.protocol}://'
if credentials and self.username:
ret += self.username
if self.password:

View file

@ -21,6 +21,23 @@ assert '<hidden>' in u.safe_full_with_username # safe version hides password
u = Uri('/local/path')
assert u.scheme == 'file'
assert u.protocol == 'file'
assert u.full == 'file:///local/path'
# Relative paths default to file, too
u = Uri('../../local/path')
assert u.scheme == 'file'
assert u.protocol == 'file'
assert u.path == '../../local/path'
# full keeps relative paths relative: '://' would re-parse the
# first segment as a host
assert u.full == 'file:../../local/path'
assert Uri(u.full).path == '../../local/path'
# An explicit file: scheme with a relative path round-trips, too
u = Uri('file:../../local/path')
assert u.path == '../../local/path'
assert u.full == 'file:../../local/path'
# Pimp returns existing Uri unchanged
u1 = Uri('ssh://host/path')