11// SPDX-License-Identifier: Apache-2.0
22// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
4- use std:: path:: Path ;
5- use std:: path:: absolute;
64use std:: sync:: Arc ;
75
86use itertools:: Itertools ;
@@ -14,6 +12,7 @@ use vortex::error::VortexResult;
1412use vortex:: error:: vortex_bail;
1513use vortex:: error:: vortex_err;
1614use vortex:: file:: multi:: MultiFileDataSource ;
15+ use vortex:: file:: multi:: parse_uri_or_path;
1716use vortex:: io:: compat:: Compat ;
1817use vortex:: io:: filesystem:: FileSystemRef ;
1918use vortex:: io:: object_store:: ObjectStoreFileSystem ;
@@ -26,38 +25,6 @@ use crate::SESSION;
2625use crate :: duckdb:: BindInputRef ;
2726use crate :: duckdb:: ExtractedValue ;
2827
29- /// Parse a glob string into a [`Url`].
30- ///
31- /// Accepts full URLs (e.g. `s3://bucket/prefix/*.vortex`, `file:///data/*.vortex`) as well as
32- /// bare file paths. For bare paths, the path is made absolute (without requiring it to exist)
33- /// so that relative paths such as `./data/*.vortex` or `../data/*.vortex` are resolved correctly.
34- fn parse_glob_url ( glob_url_str : & str ) -> VortexResult < Url > {
35- Url :: parse ( glob_url_str) . or_else ( |_| {
36- let path = absolute ( Path :: new ( glob_url_str) )
37- . map_err ( |e| vortex_err ! ( "Failed making {glob_url_str} absolute: {e}" ) ) ?;
38- // `absolute()` does not normalize `..` components, so `/a/b/../c` stays as-is.
39- // Normalizing manually avoids `..` being percent-encoded in the resulting URL.
40- let path = normalize_path ( path) ;
41- Url :: from_file_path ( path) . map_err ( |_| vortex_err ! ( "Neither URL nor path: {glob_url_str}" ) )
42- } )
43- }
44-
45- /// Normalize a path by resolving `.` and `..` components without accessing the filesystem.
46- fn normalize_path ( path : std:: path:: PathBuf ) -> std:: path:: PathBuf {
47- use std:: path:: Component ;
48- let mut normalized = std:: path:: PathBuf :: new ( ) ;
49- for component in path. components ( ) {
50- match component {
51- Component :: CurDir => { }
52- Component :: ParentDir => {
53- normalized. pop ( ) ;
54- }
55- c => normalized. push ( c) ,
56- }
57- }
58- normalized
59- }
60-
6128fn resolve_filesystem ( base_url : & Url ) -> VortexResult < FileSystemRef > {
6229 let object_store: Arc < dyn ObjectStore > = match base_url. scheme ( ) {
6330 "file" => Arc :: new ( LocalFileSystem :: new ( ) ) ,
@@ -104,7 +71,7 @@ pub fn bind_multi_file_scan(input: &BindInputRef) -> VortexResult<MultiLayoutDat
10471 // Parse each glob URL and resolve its filesystem.
10572 let mut glob_urls: Vec < Url > = Vec :: with_capacity ( glob_strings. len ( ) ) ;
10673 for glob_str in & glob_strings {
107- glob_urls. push ( parse_glob_url ( glob_str) ?) ;
74+ glob_urls. push ( parse_uri_or_path ( glob_str) ?) ;
10875 }
10976
11077 // Cache filesystems by base URL to avoid resolving the same filesystem multiple times.
@@ -134,146 +101,3 @@ pub fn bind_multi_file_scan(input: &BindInputRef) -> VortexResult<MultiLayoutDat
134101 builder. build ( ) . await
135102 } )
136103}
137-
138- #[ cfg( test) ]
139- mod tests {
140- use rstest:: rstest;
141-
142- use super :: * ;
143-
144- #[ test]
145- fn test_parse_glob_url_s3 ( ) -> VortexResult < ( ) > {
146- let url = parse_glob_url ( "s3://my-bucket/prefix/*.vortex" ) ?;
147- assert_eq ! ( url. scheme( ) , "s3" ) ;
148- assert_eq ! ( url. host_str( ) , Some ( "my-bucket" ) ) ;
149- assert_eq ! ( url. path( ) , "/prefix/*.vortex" ) ;
150- Ok ( ( ) )
151- }
152-
153- #[ test]
154- fn test_parse_glob_url_file_scheme ( ) -> VortexResult < ( ) > {
155- let url = parse_glob_url ( "file:///absolute/path/data.vortex" ) ?;
156- assert_eq ! ( url. scheme( ) , "file" ) ;
157- assert_eq ! ( url. path( ) , "/absolute/path/data.vortex" ) ;
158- Ok ( ( ) )
159- }
160-
161- #[ test]
162- fn test_parse_glob_url_absolute_glob_path ( ) -> VortexResult < ( ) > {
163- let tmpdir = tempfile:: tempdir ( ) ?;
164- let glob = format ! ( "{}/*.vortex" , tmpdir. path( ) . display( ) ) ;
165- let url = parse_glob_url ( & glob) ?;
166- assert_eq ! ( url. scheme( ) , "file" ) ;
167- assert ! ( url. path( ) . ends_with( "/*.vortex" ) ) ;
168- Ok ( ( ) )
169- }
170-
171- #[ test]
172- fn test_parse_glob_url_absolute_existing_path ( ) -> VortexResult < ( ) > {
173- let tmpfile = tempfile:: NamedTempFile :: new ( ) ?;
174- let canonical = std:: fs:: canonicalize ( tmpfile. path ( ) ) ?;
175- let path_str = canonical
176- . to_str ( )
177- . ok_or_else ( || vortex_err ! ( "canonical path is not valid UTF-8" ) ) ?;
178- let url = parse_glob_url ( path_str) ?;
179- assert_eq ! ( url. scheme( ) , "file" ) ;
180- assert_eq ! ( url. path( ) , path_str) ;
181- Ok ( ( ) )
182- }
183-
184- #[ test]
185- fn test_parse_glob_url_relative_path ( ) -> VortexResult < ( ) > {
186- // Create a tempfile in the current working directory so we can refer to it
187- // by a relative name (just the filename, without any directory component).
188- let tmpfile = tempfile:: NamedTempFile :: new_in ( "." ) ?;
189- let filename = tmpfile
190- . path ( )
191- . file_name ( )
192- . ok_or_else ( || vortex_err ! ( "temp file missing file name" ) ) ?
193- . to_str ( )
194- . ok_or_else ( || vortex_err ! ( "temp file name is not valid UTF-8" ) ) ?;
195-
196- let url = parse_glob_url ( filename) ?;
197- assert_eq ! ( url. scheme( ) , "file" ) ;
198- // The relative name must have been resolved to an absolute path.
199- assert ! ( url. path( ) . ends_with( filename) ) ;
200- assert ! ( url. path( ) . starts_with( '/' ) ) ;
201- Ok ( ( ) )
202- }
203-
204- #[ test]
205- fn test_parse_glob_url_relative_glob_path ( ) -> VortexResult < ( ) > {
206- // A relative path with a glob character (e.g. `./data/*.vortex`) must also resolve
207- // correctly.
208- let tmpdir = tempfile:: tempdir_in ( "." ) ?;
209- let dir_name = tmpdir
210- . path ( )
211- . file_name ( )
212- . ok_or_else ( || vortex_err ! ( "temp dir missing file name" ) ) ?
213- . to_str ( )
214- . ok_or_else ( || vortex_err ! ( "temp dir name is not valid UTF-8" ) ) ?;
215- let glob = format ! ( "./{dir_name}/*.vortex" ) ;
216- let url = parse_glob_url ( & glob) ?;
217- assert_eq ! ( url. scheme( ) , "file" ) ;
218- assert ! ( url. path( ) . starts_with( '/' ) ) ;
219- assert ! ( url. path( ) . ends_with( "/*.vortex" ) ) ;
220- Ok ( ( ) )
221- }
222-
223- #[ test]
224- fn test_parse_glob_url_nonexistent_path ( ) -> VortexResult < ( ) > {
225- // absolute() does not require the path to exist, so a non-existent path succeeds.
226- let url = parse_glob_url ( "/nonexistent/path/file.vortex" ) ?;
227- assert_eq ! ( url. scheme( ) , "file" ) ;
228- assert_eq ! ( url. path( ) , "/nonexistent/path/file.vortex" ) ;
229- Ok ( ( ) )
230- }
231-
232- #[ test]
233- fn test_parse_glob_url_parent_relative_path ( ) -> VortexResult < ( ) > {
234- // A path starting with `..` must be resolved to an absolute path without
235- // percent-encoding the `..` component in the resulting URL.
236- let tmpfile = tempfile:: NamedTempFile :: new_in ( ".." ) ?;
237- let filename = tmpfile
238- . path ( )
239- . file_name ( )
240- . ok_or_else ( || vortex_err ! ( "temp file missing file name" ) ) ?
241- . to_str ( )
242- . ok_or_else ( || vortex_err ! ( "temp file name is not valid UTF-8" ) ) ?;
243- let relative = format ! ( "../{filename}" ) ;
244-
245- let url = parse_glob_url ( & relative) ?;
246- assert_eq ! ( url. scheme( ) , "file" ) ;
247- // The resolved path must be absolute and must not contain encoded dots.
248- assert ! ( url. path( ) . starts_with( '/' ) ) ;
249- assert ! (
250- !url. path( ) . contains( "%2E" ) ,
251- "path must not contain percent-encoded dots"
252- ) ;
253- assert ! ( url. path( ) . ends_with( filename) ) ;
254- Ok ( ( ) )
255- }
256-
257- // Use absolute paths so the expected result is cwd-independent.
258- #[ rstest]
259- #[ case( "/a/./b" , "/a/b" ) ]
260- #[ case( "/a/b/./c" , "/a/b/c" ) ]
261- #[ case( "/a/../b" , "/b" ) ]
262- #[ case( "/a/b/../c" , "/a/c" ) ]
263- #[ case( "/a/b/../../c" , "/c" ) ]
264- #[ case( "/a/./b/.././c" , "/a/c" ) ]
265- #[ case( "/a/b/../.." , "/" ) ]
266- fn test_parse_glob_url_dot_normalization (
267- #[ case] input : & str ,
268- #[ case] expected_path : & str ,
269- ) -> VortexResult < ( ) > {
270- let url = parse_glob_url ( input) ?;
271- assert_eq ! ( url. scheme( ) , "file" ) ;
272- assert_eq ! (
273- url. path( ) ,
274- expected_path,
275- "input {input:?} should normalize to {expected_path:?}"
276- ) ;
277- Ok ( ( ) )
278- }
279- }
0 commit comments