places/
util.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::error::{Error, Result};
6use std::path::{Path, PathBuf};
7use url::Url;
8
9/// Equivalent to `&s[..max_len.min(s.len())]`, but handles the case where
10/// `s.is_char_boundary(max_len)` is false (which would otherwise panic).
11pub fn slice_up_to(s: &str, max_len: usize) -> &str {
12    if max_len >= s.len() {
13        return s;
14    }
15    let mut idx = max_len;
16    while !s.is_char_boundary(idx) {
17        idx -= 1;
18    }
19    &s[..idx]
20}
21
22/// `Path` is basically just a `str` with no validation, and so in practice it
23/// could contain a file URL. Rusqlite takes advantage of this a bit, and says
24/// `AsRef<Path>` but really means "anything sqlite can take as an argument".
25///
26/// Swift loves using file urls (the only support it has for file manipulation
27/// is through file urls), so it's handy to support them if possible.
28fn unurl_path(p: impl AsRef<Path>) -> PathBuf {
29    p.as_ref()
30        .to_str()
31        .and_then(|s| Url::parse(s).ok())
32        .and_then(|u| {
33            if u.scheme() == "file" {
34                u.to_file_path().ok()
35            } else {
36                None
37            }
38        })
39        .unwrap_or_else(|| p.as_ref().to_owned())
40}
41
42/// If `p` is a file URL, return it, otherwise try and make it one.
43///
44/// Errors if `p` is a relative non-url path, or if it's a URL path
45/// that's isn't a `file:` URL.
46pub fn ensure_url_path(p: impl AsRef<Path>) -> Result<Url> {
47    if let Some(u) = p.as_ref().to_str().and_then(|s| Url::parse(s).ok()) {
48        if u.scheme() == "file" {
49            Ok(u)
50        } else {
51            Err(Error::IllegalDatabasePath(p.as_ref().to_owned()))
52        }
53    } else {
54        let p = p.as_ref();
55        let u = Url::from_file_path(p).map_err(|_| Error::IllegalDatabasePath(p.to_owned()))?;
56        Ok(u)
57    }
58}
59
60/// As best as possible, convert `p` into an absolute path, resolving
61/// all symlinks along the way.
62///
63/// If `p` is a file url, it's converted to a path before this.
64pub fn normalize_path(p: impl AsRef<Path>) -> Result<PathBuf> {
65    let path = unurl_path(p);
66    if let Ok(canonical) = path.canonicalize() {
67        return Ok(canonical);
68    }
69    // It probably doesn't exist yet. This is an error, although it seems to
70    // work on some systems.
71    //
72    // We resolve this by trying to canonicalize the parent directory, and
73    // appending the requested file name onto that. If we can't canonicalize
74    // the parent, we return an error.
75    //
76    // Also, we return errors if the path ends in "..", if there is no
77    // parent directory, etc.
78    let file_name = path
79        .file_name()
80        .ok_or_else(|| Error::IllegalDatabasePath(path.clone()))?;
81
82    let parent = path
83        .parent()
84        .ok_or_else(|| Error::IllegalDatabasePath(path.clone()))?;
85
86    let mut canonical = parent.canonicalize()?;
87    canonical.push(file_name);
88    Ok(canonical)
89}
90
91#[cfg(test)]
92mod test {
93    use super::*;
94    #[test]
95    fn test_slice_up_to() {
96        assert_eq!(slice_up_to("abcde", 4), "abcd");
97        assert_eq!(slice_up_to("abcde", 5), "abcde");
98        assert_eq!(slice_up_to("abcde", 6), "abcde");
99        let s = "abcd😀";
100        assert_eq!(s.len(), 8);
101        assert_eq!(slice_up_to(s, 4), "abcd");
102        assert_eq!(slice_up_to(s, 5), "abcd");
103        assert_eq!(slice_up_to(s, 6), "abcd");
104        assert_eq!(slice_up_to(s, 7), "abcd");
105        assert_eq!(slice_up_to(s, 8), s);
106    }
107    #[test]
108    fn test_unurl_path() {
109        assert_eq!(
110            unurl_path("file:///foo%20bar/baz").to_string_lossy(),
111            "/foo bar/baz"
112        );
113        assert_eq!(unurl_path("/foo bar/baz").to_string_lossy(), "/foo bar/baz");
114        assert_eq!(unurl_path("../baz").to_string_lossy(), "../baz");
115    }
116
117    #[test]
118    fn test_ensure_url() {
119        assert_eq!(
120            ensure_url_path("file:///foo%20bar/baz").unwrap().as_str(),
121            "file:///foo%20bar/baz"
122        );
123
124        assert_eq!(
125            ensure_url_path("/foo bar/baz").unwrap().as_str(),
126            "file:///foo%20bar/baz"
127        );
128
129        assert!(ensure_url_path("bar").is_err());
130
131        assert!(ensure_url_path("http://www.not-a-file.com").is_err());
132    }
133}