opendal/raw/
path.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::*;
19
20/// build_abs_path will build an absolute path with root.
21///
22/// # Rules
23///
24/// - Input root MUST be the format like `/abc/def/`
25/// - Output will be the format like `path/to/root/path`.
26pub fn build_abs_path(root: &str, path: &str) -> String {
27    debug_assert!(root.starts_with('/'), "root must start with /");
28    debug_assert!(root.ends_with('/'), "root must end with /");
29
30    let p = root[1..].to_string();
31
32    if path == "/" {
33        p
34    } else {
35        debug_assert!(!path.starts_with('/'), "path must not start with /");
36        p + path
37    }
38}
39
40/// build_rooted_abs_path will build an absolute path with root.
41///
42/// # Rules
43///
44/// - Input root MUST be the format like `/abc/def/`
45/// - Output will be the format like `/path/to/root/path`.
46pub fn build_rooted_abs_path(root: &str, path: &str) -> String {
47    debug_assert!(root.starts_with('/'), "root must start with /");
48    debug_assert!(root.ends_with('/'), "root must end with /");
49
50    let p = root.to_string();
51
52    if path == "/" {
53        p
54    } else {
55        debug_assert!(!path.starts_with('/'), "path must not start with /");
56        p + path
57    }
58}
59
60/// build_rel_path will build a relative path towards root.
61///
62/// # Rules
63///
64/// - Input root MUST be the format like `/abc/def/`
65/// - Input path MUST start with root like `/abc/def/path/to/file`
66/// - Output will be the format like `path/to/file`.
67pub fn build_rel_path(root: &str, path: &str) -> String {
68    debug_assert!(root != path, "get rel path with root is invalid");
69
70    if path.starts_with('/') {
71        debug_assert!(
72            path.starts_with(root),
73            "path {path} doesn't start with root {root}"
74        );
75        path[root.len()..].to_string()
76    } else {
77        debug_assert!(
78            path.starts_with(&root[1..]),
79            "path {path} doesn't start with root {root}"
80        );
81        path[root.len() - 1..].to_string()
82    }
83}
84
85/// Make sure all operation are constructed by normalized path:
86///
87/// - Path endswith `/` means it's a dir path.
88/// - Otherwise, it's a file path.
89///
90/// # Normalize Rules
91///
92/// - All whitespace will be trimmed: ` abc/def ` => `abc/def`
93/// - All leading / will be trimmed: `///abc` => `abc`
94/// - Internal // will be replaced by /: `abc///def` => `abc/def`
95/// - Empty path will be `/`: `` => `/`
96pub fn normalize_path(path: &str) -> String {
97    // - all whitespace has been trimmed.
98    // - all leading `/` has been trimmed.
99    let path = path.trim().trim_start_matches('/');
100
101    // Fast line for empty path.
102    if path.is_empty() {
103        return "/".to_string();
104    }
105
106    let has_trailing = path.ends_with('/');
107
108    let mut p = path
109        .split('/')
110        .filter(|v| !v.is_empty())
111        .collect::<Vec<&str>>()
112        .join("/");
113
114    // Append trailing back if input path is endswith `/`.
115    if has_trailing {
116        p.push('/');
117    }
118
119    p
120}
121
122/// Make sure root is normalized to style like `/abc/def/`.
123///
124/// # Normalize Rules
125///
126/// - All whitespace will be trimmed: ` abc/def ` => `abc/def`
127/// - All leading / will be trimmed: `///abc` => `abc`
128/// - Internal // will be replaced by /: `abc///def` => `abc/def`
129/// - Empty path will be `/`: `` => `/`
130/// - Add leading `/` if not starts with: `abc/` => `/abc/`
131/// - Add trailing `/` if not ends with: `/abc` => `/abc/`
132///
133/// Finally, we will get path like `/path/to/root/`.
134pub fn normalize_root(v: &str) -> String {
135    let mut v = v
136        .split('/')
137        .filter(|v| !v.is_empty())
138        .collect::<Vec<&str>>()
139        .join("/");
140    if !v.starts_with('/') {
141        v.insert(0, '/');
142    }
143    if !v.ends_with('/') {
144        v.push('/')
145    }
146    v
147}
148
149/// Get basename from path.
150pub fn get_basename(path: &str) -> &str {
151    // Handle root case
152    if path == "/" {
153        return "/";
154    }
155
156    // Handle file case
157    if !path.ends_with('/') {
158        return path
159            .split('/')
160            .next_back()
161            .expect("file path without name is invalid");
162    }
163
164    // The idx of second `/` if path in reserve order.
165    // - `abc/` => `None`
166    // - `abc/def/` => `Some(3)`
167    let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
168
169    match idx {
170        Some(v) => {
171            let (_, name) = path.split_at(v);
172            name
173        }
174        None => path,
175    }
176}
177
178/// Get parent from path.
179pub fn get_parent(path: &str) -> &str {
180    if path == "/" {
181        return "/";
182    }
183
184    if !path.ends_with('/') {
185        // The idx of first `/` if path in reserve order.
186        // - `abc` => `None`
187        // - `abc/def` => `Some(3)`
188        let idx = path.rfind('/');
189
190        return match idx {
191            Some(v) => {
192                let (parent, _) = path.split_at(v + 1);
193                parent
194            }
195            None => "/",
196        };
197    }
198
199    // The idx of second `/` if path in reserve order.
200    // - `abc/` => `None`
201    // - `abc/def/` => `Some(3)`
202    let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
203
204    match idx {
205        Some(v) => {
206            let (parent, _) = path.split_at(v);
207            parent
208        }
209        None => "/",
210    }
211}
212
213/// Validate given path is match with given EntryMode.
214#[inline]
215pub fn validate_path(path: &str, mode: EntryMode) -> bool {
216    debug_assert!(!path.is_empty(), "input path should not be empty");
217
218    match mode {
219        EntryMode::FILE => !path.ends_with('/'),
220        EntryMode::DIR => path.ends_with('/'),
221        EntryMode::Unknown => false,
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_normalize_path() {
231        let cases = vec![
232            ("file path", "abc", "abc"),
233            ("dir path", "abc/", "abc/"),
234            ("empty path", "", "/"),
235            ("root path", "/", "/"),
236            ("root path with extra /", "///", "/"),
237            ("abs file path", "/abc/def", "abc/def"),
238            ("abs dir path", "/abc/def/", "abc/def/"),
239            ("abs file path with extra /", "///abc/def", "abc/def"),
240            ("abs dir path with extra /", "///abc/def/", "abc/def/"),
241            ("file path contains ///", "abc///def", "abc/def"),
242            ("dir path contains ///", "abc///def///", "abc/def/"),
243            ("file with whitespace", "abc/def   ", "abc/def"),
244        ];
245
246        for (name, input, expect) in cases {
247            assert_eq!(normalize_path(input), expect, "{name}")
248        }
249    }
250
251    #[test]
252    fn test_normalize_root() {
253        let cases = vec![
254            ("dir path", "abc/", "/abc/"),
255            ("empty path", "", "/"),
256            ("root path", "/", "/"),
257            ("root path with extra /", "///", "/"),
258            ("abs dir path", "/abc/def/", "/abc/def/"),
259            ("abs file path with extra /", "///abc/def", "/abc/def/"),
260            ("abs dir path with extra /", "///abc/def/", "/abc/def/"),
261            ("dir path contains ///", "abc///def///", "/abc/def/"),
262        ];
263
264        for (name, input, expect) in cases {
265            assert_eq!(normalize_root(input), expect, "{name}")
266        }
267    }
268
269    #[test]
270    fn test_get_basename() {
271        let cases = vec![
272            ("file abs path", "foo/bar/baz.txt", "baz.txt"),
273            ("file rel path", "bar/baz.txt", "baz.txt"),
274            ("file walk", "foo/bar/baz", "baz"),
275            ("dir rel path", "bar/baz/", "baz/"),
276            ("dir root", "/", "/"),
277            ("dir walk", "foo/bar/baz/", "baz/"),
278        ];
279
280        for (name, input, expect) in cases {
281            let actual = get_basename(input);
282            assert_eq!(actual, expect, "{name}")
283        }
284    }
285
286    #[test]
287    fn test_get_parent() {
288        let cases = vec![
289            ("file abs path", "foo/bar/baz.txt", "foo/bar/"),
290            ("file rel path", "bar/baz.txt", "bar/"),
291            ("file walk", "foo/bar/baz", "foo/bar/"),
292            ("dir rel path", "bar/baz/", "bar/"),
293            ("dir root", "/", "/"),
294            ("dir abs path", "/foo/bar/", "/foo/"),
295            ("dir walk", "foo/bar/baz/", "foo/bar/"),
296        ];
297
298        for (name, input, expect) in cases {
299            let actual = get_parent(input);
300            assert_eq!(actual, expect, "{name}")
301        }
302    }
303
304    #[test]
305    fn test_build_abs_path() {
306        let cases = vec![
307            ("input abs file", "/abc/", "/", "abc/"),
308            ("input dir", "/abc/", "def/", "abc/def/"),
309            ("input file", "/abc/", "def", "abc/def"),
310            ("input abs file with root /", "/", "/", ""),
311            ("input empty with root /", "/", "", ""),
312            ("input dir with root /", "/", "def/", "def/"),
313            ("input file with root /", "/", "def", "def"),
314        ];
315
316        for (name, root, input, expect) in cases {
317            let actual = build_abs_path(root, input);
318            assert_eq!(actual, expect, "{name}")
319        }
320    }
321
322    #[test]
323    fn test_build_rooted_abs_path() {
324        let cases = vec![
325            ("input abs file", "/abc/", "/", "/abc/"),
326            ("input dir", "/abc/", "def/", "/abc/def/"),
327            ("input file", "/abc/", "def", "/abc/def"),
328            ("input abs file with root /", "/", "/", "/"),
329            ("input dir with root /", "/", "def/", "/def/"),
330            ("input file with root /", "/", "def", "/def"),
331        ];
332
333        for (name, root, input, expect) in cases {
334            let actual = build_rooted_abs_path(root, input);
335            assert_eq!(actual, expect, "{name}")
336        }
337    }
338
339    #[test]
340    fn test_build_rel_path() {
341        let cases = vec![
342            ("input abs file", "/abc/", "/abc/def", "def"),
343            ("input dir", "/abc/", "/abc/def/", "def/"),
344            ("input file", "/abc/", "abc/def", "def"),
345            ("input dir with root /", "/", "def/", "def/"),
346            ("input file with root /", "/", "def", "def"),
347        ];
348
349        for (name, root, input, expect) in cases {
350            let actual = build_rel_path(root, input);
351            assert_eq!(actual, expect, "{name}")
352        }
353    }
354
355    #[test]
356    fn test_validate_path() {
357        let cases = vec![
358            ("input file with mode file", "abc", EntryMode::FILE, true),
359            ("input file with mode dir", "abc", EntryMode::DIR, false),
360            ("input dir with mode file", "abc/", EntryMode::FILE, false),
361            ("input dir with mode dir", "abc/", EntryMode::DIR, true),
362            ("root with mode dir", "/", EntryMode::DIR, true),
363            (
364                "input file with mode unknown",
365                "abc",
366                EntryMode::Unknown,
367                false,
368            ),
369            (
370                "input dir with mode unknown",
371                "abc/",
372                EntryMode::Unknown,
373                false,
374            ),
375        ];
376
377        for (name, path, mode, expect) in cases {
378            let actual = validate_path(path, mode);
379            assert_eq!(actual, expect, "{name}")
380        }
381    }
382}