1use std::path::{Path, PathBuf};
6
7use url::Url;
8
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11 #[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
23fn 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#[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 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 let bytes = unsafe { CStr::from_ptr(resolved) }.to_bytes().to_vec();
68 Ok(PathBuf::from(OsString::from_vec(bytes)))
69}
70
71pub 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 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}