1use std::borrow::Cow;
5use std::env::current_dir;
6use std::ffi::OsStr;
7use std::fs::canonicalize;
8use std::io;
9use std::io::ErrorKind;
10use std::os::unix::ffi::OsStrExt as _;
11use std::path::Component;
12use std::path::Path;
13use std::path::PathBuf;
14
15use anyhow::anyhow;
16use anyhow::Context as _;
17use anyhow::Result;
18
19use dirs::cache_dir;
20use dirs::config_dir;
21
22
23fn normalize(path: &Path) -> PathBuf {
31 let components = path.components();
32 let path = PathBuf::with_capacity(path.as_os_str().len());
33
34 let mut path = components.fold(path, |mut path, component| {
35 match component {
36 Component::Prefix(..) | Component::RootDir => (),
37 Component::CurDir => return path,
38 Component::ParentDir => {
39 if let Some(prev) = path.components().next_back() {
40 match prev {
41 Component::CurDir => {
42 unreachable!()
46 },
47 Component::Prefix(..) | Component::RootDir | Component::ParentDir => (),
48 Component::Normal(..) => {
49 path.pop();
50 return path
51 },
52 }
53 }
54 },
55 Component::Normal(c) => {
56 path.push(c);
57 return path
58 },
59 }
60
61 path.push(component.as_os_str());
62 path
63 });
64
65 let () = path.shrink_to_fit();
66 path
67}
68
69
70fn canonicalize_non_strict(path: &Path) -> io::Result<PathBuf> {
76 let mut path = path;
77 let input = path;
78
79 let resolved = loop {
80 match canonicalize(path) {
81 Ok(resolved) => break Cow::Owned(resolved),
82 Err(err) if err.kind() == ErrorKind::NotFound => (),
83 e => return e,
84 }
85
86 match path.parent() {
87 None => {
88 path = Path::new("");
91 break Cow::Borrowed(path)
92 },
93 Some(parent) if parent == Path::new("") => {
94 path = parent;
97 break Cow::Owned(current_dir()?)
98 },
99 Some(parent) => {
100 let parent_len = parent.as_os_str().as_bytes().len();
104 let path_bytes = path.as_os_str().as_bytes();
105 path = Path::new(OsStr::from_bytes(
110 path_bytes
111 .get(parent_len + 1..)
112 .expect("constructed path has no trailing separator"),
113 ));
114 },
115 }
116 };
117
118 let input_bytes = input.as_os_str().as_bytes();
119 let path_len = path.as_os_str().as_bytes().len();
120 let unresolved = input_bytes
123 .get(path_len..)
124 .expect("failed to access input path sub-string");
125 let complete = resolved.join(OsStr::from_bytes(unresolved));
126 let normalized = normalize(&complete);
131 Ok(normalized)
132}
133
134
135#[derive(Debug)]
137pub struct Paths {
138 config_dir: PathBuf,
142 state_dir: PathBuf,
144}
145
146impl Paths {
147 pub fn new(config_dir: Option<PathBuf>) -> Result<Self> {
150 let config_dir = if let Some(config_dir) = config_dir {
151 config_dir
152 } else {
153 self::config_dir()
154 .ok_or_else(|| anyhow!("unable to determine config directory"))?
155 .join("notnow")
156 };
157 let config_dir = canonicalize_non_strict(&config_dir)
158 .with_context(|| format!("failed to canonicalize path `{}`", config_dir.display()))?;
159
160 let mut config_dir_rel = config_dir.components();
161 let _root = config_dir_rel.next();
162 let config_dir_rel = config_dir_rel.as_path();
163 debug_assert!(config_dir_rel.is_relative(), "{config_dir_rel:?}");
164
165 let state_dir = cache_dir()
166 .ok_or_else(|| anyhow!("unable to determine cache directory"))?
167 .join("notnow")
168 .join(config_dir_rel);
169
170 let slf = Self {
171 config_dir,
172 state_dir,
173 };
174 Ok(slf)
175 }
176
177 pub fn ui_config_dir(&self) -> &Path {
179 &self.config_dir
180 }
181
182 pub fn ui_config_file(&self) -> &OsStr {
184 OsStr::new("notnow.json")
185 }
186
187 pub fn tasks_dir(&self) -> PathBuf {
189 self.ui_config_dir().join("tasks")
190 }
191
192 pub fn ui_state_dir(&self) -> &Path {
194 &self.state_dir
195 }
196
197 pub fn ui_state_file(&self) -> &OsStr {
199 OsStr::new("ui-state.json")
200 }
201
202 pub(crate) fn lock_file(&self) -> PathBuf {
204 self.state_dir.join("notnow.lock")
205 }
206}
207
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 use tempfile::TempDir;
214
215
216 #[test]
218 fn path_normalization() {
219 assert_eq!(normalize(Path::new("tmp/foobar/..")), Path::new("tmp"));
220 assert_eq!(normalize(Path::new("/tmp/foobar/..")), Path::new("/tmp"));
221 assert_eq!(normalize(Path::new("/tmp/.")), Path::new("/tmp"));
222 assert_eq!(normalize(Path::new("/tmp/./blah")), Path::new("/tmp/blah"));
223 assert_eq!(normalize(Path::new("/tmp/../blah")), Path::new("/blah"));
224 assert_eq!(normalize(Path::new("./foo")), Path::new("foo"));
225 assert_eq!(
226 normalize(Path::new("./foo/")).as_os_str(),
227 Path::new("foo").as_os_str()
228 );
229 assert_eq!(normalize(Path::new("foo")), Path::new("foo"));
230 assert_eq!(
231 normalize(Path::new("foo/")).as_os_str(),
232 Path::new("foo").as_os_str()
233 );
234 assert_eq!(normalize(Path::new("../foo")), Path::new("../foo"));
235 assert_eq!(normalize(Path::new("../foo/")), Path::new("../foo"));
236 assert_eq!(
237 normalize(Path::new("./././relative-dir-that-does-not-exist/../file")),
238 Path::new("file")
239 );
240 }
241
242 #[test]
244 fn non_strict_canonicalization() {
245 let dir = current_dir().unwrap();
246 let path = Path::new("relative-path-that-does-not-exist");
247 let real = canonicalize_non_strict(path).unwrap();
248 assert_eq!(real, dir.join(path));
249
250 let dir = current_dir().unwrap();
251 let path = Path::new("relative-path-that-does-not-exist/");
252 let real = canonicalize_non_strict(path).unwrap();
253 assert_eq!(
254 real.as_os_str(),
255 dir
256 .join(Path::new("relative-path-that-does-not-exist"))
257 .as_os_str()
258 );
259
260 let path = Path::new("relative-dir-that-does-not-exist/file");
261 let real = canonicalize_non_strict(path).unwrap();
262 assert_eq!(real, dir.join(path));
263
264 let path = Path::new("./relative-dir-that-does-not-exist/file");
265 let real = canonicalize_non_strict(path).unwrap();
266 assert_eq!(real, dir.join(normalize(path)));
267
268 let path = Path::new("./././relative-dir-that-does-not-exist/../file");
269 let real = canonicalize_non_strict(path).unwrap();
270 assert_eq!(real, dir.join("file"));
271
272 let path = Path::new("../relative-path-that-does-not-exist");
273 let real = canonicalize_non_strict(path).unwrap();
274 assert_eq!(
275 real,
276 dir
277 .parent()
278 .unwrap()
279 .join("relative-path-that-does-not-exist")
280 );
281
282 let path = Path::new("../relative-dir-that-does-not-exist/file");
283 let real = canonicalize_non_strict(path).unwrap();
284 assert_eq!(
285 real,
286 dir
287 .parent()
288 .unwrap()
289 .join("relative-dir-that-does-not-exist/file")
290 );
291
292 let path = Path::new("/absolute-path-that-does-not-exist");
293 let real = canonicalize_non_strict(path).unwrap();
294 assert_eq!(real, path);
295
296 let path = Path::new("/absolute-dir-that-does-not-exist/file");
297 let real = canonicalize_non_strict(path).unwrap();
298 assert_eq!(real, path);
299
300 let dir = TempDir::new().unwrap();
301 let dir = dir.path();
302
303 let path = dir;
304 let real = canonicalize_non_strict(path).unwrap();
305 assert_eq!(real, path);
306
307 let path = dir.join("foobar");
308 let real = canonicalize_non_strict(&path).unwrap();
309 assert_eq!(real, path);
310 }
311
312 #[test]
314 fn paths_instantiation() {
315 let _paths = Paths::new(None).unwrap();
316
317 let dir = TempDir::new().unwrap();
318 let path = dir.path().join("i").join("do").join("not").join("exist");
319 let _paths = Paths::new(Some(path)).unwrap();
320 }
321}