@@ -20,22 +20,26 @@ pub fn normalize_url(url: &str) -> Result<String, iri_string::types::CreationErr
2020
2121 let path = iri. path_str ( ) ;
2222
23- if scheme == "file"
24- && let Some ( rest) = path. strip_prefix ( "~/" )
25- {
23+ // TODO: utilize `path.normalize_lexically()` once it stabilizes
24+ // https://github.com/rust-lang/rust/issues/134694
25+
26+ if scheme == "file" && path. starts_with ( "~/" ) {
27+ let rest = path. strip_prefix ( "~/" ) . unwrap ( ) ; // safe, the prefix was checked just above
28+
2629 let home_dir = std:: env:: home_dir ( ) . expect ( "unable to determine home directory" ) ;
27- let path2 = home_dir. join ( rest) ;
28- let path3 = std:: path:: absolute ( & path2) . unwrap_or ( path2) ;
29- let path4 = path3. to_str ( ) . unwrap_or ( path) ;
30-
31- write ! ( & mut out, "{}" , path4) . unwrap ( ) ;
32- } else if scheme == "file" && !path. starts_with ( '/' ) {
33- let cur_dir = std:: env:: current_dir ( ) . expect ( "unable to determine current directory" ) ;
34- let path2 = cur_dir. join ( path) ;
35- let path3 = std:: path:: absolute ( & path2) . unwrap_or ( path2) ;
36- let path4 = path3. to_str ( ) . unwrap_or ( path) ;
37-
38- write ! ( & mut out, "{}" , path4) . unwrap ( ) ;
30+
31+ let path = home_dir. join ( rest) ;
32+ let path = std:: path:: absolute ( & path) . unwrap_or ( path) ;
33+ let path = path. canonicalize ( ) . unwrap_or ( path) ;
34+
35+ write ! ( & mut out, "{}" , path. display( ) ) . unwrap ( ) ;
36+ } else if scheme == "file" {
37+ // `std::path::absolute` also changes relative paths to absolute with the current directory
38+ // as base.
39+ let path = std:: path:: absolute ( path) . unwrap_or_else ( |_| std:: path:: PathBuf :: from ( path) ) ;
40+ let path = path. canonicalize ( ) . unwrap_or ( path) ;
41+
42+ write ! ( & mut out, "{}" , path. display( ) ) . unwrap ( ) ;
3943 } else if iri. authority_str ( ) . is_some ( ) && path. is_empty ( ) {
4044 write ! ( & mut out, "/" ) . unwrap ( ) ;
4145 } else {
@@ -56,17 +60,20 @@ pub fn normalize_url(url: &str) -> Result<String, iri_string::types::CreationErr
5660#[ cfg( test) ]
5761mod tests {
5862 use super :: * ;
59- use std:: string:: ToString ;
63+ use std:: { format , string:: ToString } ;
6064
6165 #[ test]
6266 fn url_normalization ( ) {
6367 let cases = [
6468 ( "https://example.org" , "https://example.org/" ) ,
6569 ( "https://example.org/" , "https://example.org/" ) ,
6670 ( "http://example.com/path" , "http://example.com/path" ) ,
71+ ( "https://api.example.com" , "https://api.example.com/" ) ,
72+ ( "http://localhost:3000" , "http://localhost:3000/" ) ,
73+ ( "ftp://fileserver.local" , "ftp://fileserver.local/" ) ,
6774 (
68- "https://user:pass@example.org:8080/path?query=value #fragment" ,
69- "https://user:pass@example.org:8080/path?query=value #fragment" ,
75+ "https://user:pass@example.org:8080/path?foo=bar& query=hello world #fragment" ,
76+ "https://user:pass@example.org:8080/path?foo=bar& query=hello%20world #fragment" ,
7077 ) ,
7178 ( "near://testnet/123456789" , "near://testnet/123456789" ) ,
7279 (
@@ -88,23 +95,50 @@ mod tests {
8895 "https://example.org/path%20already%20encoded" ,
8996 ) ,
9097 (
91- "https://example.org/?q=test&foo=bar " ,
92- "https://example.org/?q=test&foo=bar " ,
98+ "data:text/plain;base64,SGVsbG8= " ,
99+ "data:text/plain;base64,SGVsbG8= " ,
93100 ) ,
101+ ( "tel:+1-555-123-4567" , "tel:+1-555-123-4567" ) ,
102+ ( "urn:isbn:1234567890" , "urn:isbn:1234567890" ) ,
94103 (
95- "https://example.org/page#section1" ,
96- "https://example.org/page#section1" ,
104+ // Plain strings get `file:` scheme and current directory prepended
105+ "document.txt" ,
106+ & format ! (
107+ "file:{}/document.txt" ,
108+ std:: env:: current_dir( ) . unwrap( ) . display( )
109+ ) ,
97110 ) ,
98111 (
99- "https://example.org/search?q=hello world" ,
100- "https://example.org/search?q=hello%20world" ,
112+ // Domain-like strings without scheme get treated as files
113+ "example.org" ,
114+ & format ! (
115+ "file:{}/example.org" ,
116+ std:: env:: current_dir( ) . unwrap( ) . display( )
117+ ) ,
101118 ) ,
119+ // TODO: should this be inferred?
120+ // ("localhost:8080", "http://localhost:8080".into()),
102121 (
103- "data:text/plain;base64,SGVsbG8=" ,
104- "data:text/plain;base64,SGVsbG8=" ,
122+ "folder name/file.txt" ,
123+ & format ! (
124+ "file:{}/folder%20name/file.txt" ,
125+ std:: env:: current_dir( ) . unwrap( ) . display( )
126+ ) ,
127+ ) ,
128+ (
129+ "./subfolder/../file.txt" ,
130+ & format ! (
131+ "file:{}/subfolder/../file.txt" ,
132+ std:: env:: current_dir( ) . unwrap( ) . display( )
133+ ) ,
134+ ) ,
135+ (
136+ "../parent/file.txt" ,
137+ & format ! (
138+ "file:{}/../parent/file.txt" ,
139+ std:: env:: current_dir( ) . unwrap( ) . display( )
140+ ) ,
105141 ) ,
106- ( "tel:+1-555-123-4567" , "tel:+1-555-123-4567" ) ,
107- ( "urn:isbn:1234567890" , "urn:isbn:1234567890" ) ,
108142 ] ;
109143
110144 for case in cases {
@@ -173,15 +207,6 @@ mod tests {
173207 "non-path-looking input should be treated as a file in current directory, input: {:?}" ,
174208 input
175209 ) ;
176-
177- // let input = "hello\\ world!";
178- // let want = "file:".to_string() + &cur_dir + "/hello%5C%20world!";
179- // assert_eq!(
180- // normalize_url(input).unwrap(),
181- // want,
182- // "output should be url encoded, input: {:?}",
183- // input
184- // );
185210 }
186211
187212 #[ cfg( windows) ]
@@ -191,11 +216,11 @@ mod tests {
191216 let cases = [
192217 (
193218 "/file with spaces.txt" ,
194- format ! ( "file:/// {drive}:/file%20with%20spaces.txt" ) ,
219+ format ! ( "file:/{drive}:/file%20with%20spaces.txt" ) ,
195220 ) ,
196221 (
197222 "/file+with+pluses.txt" ,
198- format ! ( "file:/// {drive}:/file+with+pluses.txt" ) ,
223+ format ! ( "file:/{drive}:/file+with+pluses.txt" ) ,
199224 ) ,
200225 ] ;
201226
0 commit comments