sql_support/
path.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 std::path::{Path, PathBuf};
6
7use url::Url;
8
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    // This will happen if you provide something absurd like
12    // "/" or "" as your database path. For more subtley broken paths,
13    // we'll likely return an IoError.
14    #[error("Illegal database path: {0:?}")]
15    IllegalDatabasePath(PathBuf),
16
17    #[error("IO error: {0}")]
18    IoError(#[from] std::io::Error),
19}
20
21pub type Result<T> = std::result::Result<T, Error>;
22
23/// `Path` is basically just a `str` with no validation, and so in practice it
24/// could contain a file URL. Rusqlite takes advantage of this a bit, and says
25/// `AsRef<Path>` but really means "anything sqlite can take as an argument".
26///
27/// Swift loves using file urls (the only support it has for file manipulation
28/// is through file urls), so it's handy to support them if possible.
29fn unurl_path(p: impl AsRef<Path>) -> PathBuf {
30    p.as_ref()
31        .to_str()
32        .and_then(|s| Url::parse(s).ok())
33        .and_then(|u| {
34            if u.scheme() == "file" {
35                u.to_file_path().ok()
36            } else {
37                None
38            }
39        })
40        .unwrap_or_else(|| p.as_ref().to_owned())
41}
42
43#[cfg(not(target_os = "android"))]
44fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
45    path.canonicalize()
46}
47
48/// `std::fs::canonicalize` asks `realpath` to allocate the resolved path and
49/// releases it with `free`. On Android those are not the same allocator when
50/// the caller links against mozglue, because bionic keeps its internal
51/// `malloc` calls to itself while `free` resolves to mozjemalloc. Passing a
52/// `PATH_MAX` buffer keeps the result caller owned.
53#[cfg(target_os = "android")]
54fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
55    use std::ffi::{CStr, CString, OsString};
56    use std::os::unix::ffi::{OsStrExt, OsStringExt};
57
58    let path = CString::new(path.as_os_str().as_bytes())?;
59    let mut buffer = [0 as libc::c_char; libc::PATH_MAX as usize];
60    // SAFETY: `buffer` is the `PATH_MAX` bytes `realpath` requires, and `path`
61    // is a valid NUL terminated string for the duration of the call.
62    let resolved = unsafe { libc::realpath(path.as_ptr(), buffer.as_mut_ptr()) };
63    if resolved.is_null() {
64        return Err(std::io::Error::last_os_error());
65    }
66    // SAFETY: on success `realpath` returns `buffer`, NUL terminated.
67    let bytes = unsafe { CStr::from_ptr(resolved) }.to_bytes().to_vec();
68    Ok(PathBuf::from(OsString::from_vec(bytes)))
69}
70
71/// As best as possible, convert `p` into an absolute path, resolving
72/// all symlinks along the way.
73///
74/// If `p` is a file url, it's converted to a path before this.
75pub fn normalize_database_path(p: impl AsRef<Path>) -> Result<PathBuf> {
76    let path = unurl_path(p);
77    if let Ok(canonical) = canonicalize(&path) {
78        return Ok(canonical);
79    }
80    // It probably doesn't exist yet. This is an error, although it seems to
81    // work on some systems.
82    //
83    // We resolve this by trying to canonicalize the parent directory, and
84    // appending the requested file name onto that. If we can't canonicalize
85    // the parent, we return an error.
86    //
87    // Also, we return errors if the path ends in "..", if there is no
88    // parent directory, etc.
89    let file_name = path
90        .file_name()
91        .ok_or_else(|| Error::IllegalDatabasePath(path.clone()))?;
92
93    let parent = path
94        .parent()
95        .ok_or_else(|| Error::IllegalDatabasePath(path.clone()))?;
96
97    let mut canonical = canonicalize(parent)?;
98    canonical.push(file_name);
99    Ok(canonical)
100}
101
102#[cfg(test)]
103mod test {
104    use super::*;
105
106    #[cfg(unix)]
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_normalize_existing_file() {
119        let dir = tempfile::tempdir().unwrap();
120        let path = dir.path().join("places.sqlite");
121        std::fs::write(&path, b"").unwrap();
122
123        assert_eq!(
124            normalize_database_path(&path).unwrap(),
125            canonicalize(&path).unwrap()
126        );
127    }
128
129    #[test]
130    fn test_normalize_nonexistent_leaf() {
131        let dir = tempfile::tempdir().unwrap();
132        let path = dir.path().join("places.sqlite");
133
134        assert_eq!(
135            normalize_database_path(&path).unwrap(),
136            canonicalize(dir.path()).unwrap().join("places.sqlite")
137        );
138    }
139
140    #[test]
141    fn test_normalize_nonexistent_parent() {
142        let dir = tempfile::tempdir().unwrap();
143        let path = dir.path().join("missing").join("places.sqlite");
144
145        assert!(matches!(
146            normalize_database_path(&path),
147            Err(Error::IoError(_))
148        ));
149    }
150
151    #[test]
152    fn test_normalize_malformed_path() {
153        assert!(matches!(
154            normalize_database_path(""),
155            Err(Error::IllegalDatabasePath(_))
156        ));
157    }
158
159    #[cfg(unix)]
160    #[test]
161    fn test_normalize_file_url() {
162        let dir = tempfile::tempdir().unwrap();
163        let path = dir.path().join("places.sqlite");
164        std::fs::write(&path, b"").unwrap();
165        let url = Url::from_file_path(&path).unwrap();
166
167        assert_eq!(
168            normalize_database_path(url.as_str()).unwrap(),
169            canonicalize(&path).unwrap()
170        );
171    }
172
173    #[cfg(unix)]
174    #[test]
175    fn test_normalize_resolves_symlink() {
176        let dir = tempfile::tempdir().unwrap();
177        let target = dir.path().join("places.sqlite");
178        let link = dir.path().join("link.sqlite");
179        std::fs::write(&target, b"").unwrap();
180        std::os::unix::fs::symlink(&target, &link).unwrap();
181
182        assert_eq!(
183            normalize_database_path(&link).unwrap(),
184            canonicalize(&target).unwrap()
185        );
186    }
187}