1#[cfg(any(
8 unix,
9 windows,
10 target_os = "redox",
11 target_os = "wasi",
12 target_os = "hermit"
13))]
14use std::path::Component;
15#[cfg(any(
16 unix,
17 windows,
18 target_os = "redox",
19 target_os = "wasi",
20 target_os = "hermit"
21))]
22use std::path::Path;
23#[cfg(any(
24 unix,
25 windows,
26 target_os = "redox",
27 target_os = "wasi",
28 target_os = "hermit"
29))]
30use std::path::PathBuf;
31#[cfg(any(
32 unix,
33 windows,
34 target_os = "redox",
35 target_os = "wasi",
36 target_os = "hermit"
37))]
38use std::path::absolute;
39
40use url::Url;
41use vortex_error::VortexResult;
42use vortex_error::vortex_err;
43
44pub fn parse_uri_or_path(uri_or_path: &str) -> VortexResult<Url> {
52 if let Ok(url) = Url::parse(uri_or_path)
56 && url.scheme().len() > 1
57 {
58 return Ok(url);
59 }
60 file_path_to_url(uri_or_path)
61}
62
63#[cfg(any(
67 unix,
68 windows,
69 target_os = "redox",
70 target_os = "wasi",
71 target_os = "hermit"
72))]
73fn file_path_to_url(path: &str) -> VortexResult<Url> {
74 let abs =
75 absolute(Path::new(path)).map_err(|e| vortex_err!("failed to absolutize {path}: {e}"))?;
76 Url::from_file_path(normalize_path(abs))
77 .map_err(|_| vortex_err!("neither URL nor path: {path}"))
78}
79
80#[cfg(not(any(
83 unix,
84 windows,
85 target_os = "redox",
86 target_os = "wasi",
87 target_os = "hermit"
88)))]
89fn file_path_to_url(path: &str) -> VortexResult<Url> {
90 Err(vortex_err!(
91 "bare file paths are not supported on this platform: {path}"
92 ))
93}
94
95#[cfg(any(
97 unix,
98 windows,
99 target_os = "redox",
100 target_os = "wasi",
101 target_os = "hermit"
102))]
103fn normalize_path(path: PathBuf) -> PathBuf {
104 let mut out = PathBuf::new();
105 for component in path.components() {
106 match component {
107 Component::CurDir => {}
108 Component::ParentDir => {
109 out.pop();
110 }
111 c => out.push(c),
112 }
113 }
114 out
115}
116
117#[cfg(test)]
118mod tests {
119 use rstest::rstest;
120
121 use super::*;
122
123 #[test]
124 fn test_full_url() -> VortexResult<()> {
125 let url = parse_uri_or_path("s3://bucket/prefix/*.vortex")?;
126 assert_eq!(url.scheme(), "s3");
127 assert_eq!(url.host_str(), Some("bucket"));
128 assert_eq!(url.path(), "/prefix/*.vortex");
129 Ok(())
130 }
131
132 #[test]
133 fn test_file_scheme() -> VortexResult<()> {
134 let url = parse_uri_or_path("file:///absolute/path/data.vortex")?;
135 assert_eq!(url.scheme(), "file");
136 assert_eq!(url.path(), "/absolute/path/data.vortex");
137 Ok(())
138 }
139
140 #[test]
141 fn test_absolute_path() -> VortexResult<()> {
142 #[cfg(unix)]
145 let (input, expected_path) = ("/tmp/data/*.vortex", "/tmp/data/*.vortex");
146 #[cfg(windows)]
147 let (input, expected_path) = (r"C:\tmp\data\*.vortex", "/C:/tmp/data/*.vortex");
148 let url = parse_uri_or_path(input)?;
149 assert_eq!(url.scheme(), "file");
150 assert_eq!(url.path(), expected_path);
151 Ok(())
152 }
153
154 #[test]
155 fn test_relative_path_resolves_against_cwd() -> VortexResult<()> {
156 for input in ["data/table", "./data/table", "../data/table"] {
157 let url = parse_uri_or_path(input)?;
158 assert_eq!(url.scheme(), "file");
159 assert!(url.path().starts_with('/'));
160 assert!(url.path().ends_with("/data/table"));
161 assert!(
162 !url.path().contains("%2E"),
163 "path must not contain percent-encoded dots"
164 );
165 }
166 Ok(())
167 }
168
169 #[test]
170 fn test_single_letter_scheme_is_path() -> VortexResult<()> {
171 let url = parse_uri_or_path(r"C:\tmp\data\*.vortex")?;
175 assert_eq!(url.scheme(), "file");
176 assert_ne!(url.scheme(), "c");
177 Ok(())
178 }
179
180 #[cfg(unix)]
182 #[rstest]
183 #[case("/a/./b", "/a/b")]
184 #[case("/a/b/./c", "/a/b/c")]
185 #[case("/a/../b", "/b")]
186 #[case("/a/b/../c", "/a/c")]
187 #[case("/a/b/../../c", "/c")]
188 #[case("/a/./b/.././c", "/a/c")]
189 #[case("/a/b/../..", "/")]
190 fn test_dot_normalization(
191 #[case] input: &str,
192 #[case] expected_path: &str,
193 ) -> VortexResult<()> {
194 let url = parse_uri_or_path(input)?;
195 assert_eq!(url.scheme(), "file");
196 assert_eq!(
197 url.path(),
198 expected_path,
199 "input {input:?} should normalize to {expected_path:?}"
200 );
201 Ok(())
202 }
203}