use std::borrow::Cow;
use std::env::current_dir;
use std::ffi::OsStr;
use std::fs::canonicalize;
use std::io;
use std::io::ErrorKind;
use std::os::unix::ffi::OsStrExt as _;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::Context as _;
use anyhow::Result;
use dirs::cache_dir;
use dirs::config_dir;
fn normalize(path: &Path) -> PathBuf {
let components = path.components();
let path = PathBuf::with_capacity(path.as_os_str().len());
let mut path = components.fold(path, |mut path, component| {
match component {
Component::Prefix(..) | Component::RootDir => (),
Component::CurDir => return path,
Component::ParentDir => {
if let Some(prev) = path.components().next_back() {
match prev {
Component::CurDir => {
unreachable!()
},
Component::Prefix(..) | Component::RootDir | Component::ParentDir => (),
Component::Normal(..) => {
path.pop();
return path
},
}
}
},
Component::Normal(c) => {
path.push(c);
return path
},
}
path.push(component.as_os_str());
path
});
let () = path.shrink_to_fit();
path
}
fn canonicalize_non_strict(path: &Path) -> io::Result<PathBuf> {
let mut path = path;
let input = path;
let resolved = loop {
match canonicalize(path) {
Ok(resolved) => break Cow::Owned(resolved),
Err(err) if err.kind() == ErrorKind::NotFound => (),
e => return e,
}
match path.parent() {
None => {
path = Path::new("");
break Cow::Borrowed(path)
},
Some(parent) if parent == Path::new("") => {
path = parent;
break Cow::Owned(current_dir()?)
},
Some(parent) => {
let parent_len = parent.as_os_str().as_bytes().len();
let path_bytes = path.as_os_str().as_bytes();
path = Path::new(OsStr::from_bytes(
path_bytes
.get(parent_len + 1..)
.expect("constructed path has no trailing separator"),
));
},
}
};
let input_bytes = input.as_os_str().as_bytes();
let path_len = path.as_os_str().as_bytes().len();
let unresolved = input_bytes
.get(path_len..)
.expect("failed to access input path sub-string");
let complete = resolved.join(OsStr::from_bytes(unresolved));
let normalized = normalize(&complete);
Ok(normalized)
}
#[derive(Debug)]
pub struct Paths {
config_dir: PathBuf,
state_dir: PathBuf,
}
impl Paths {
pub fn new(config_dir: Option<PathBuf>) -> Result<Self> {
let config_dir = if let Some(config_dir) = config_dir {
config_dir
} else {
self::config_dir()
.ok_or_else(|| anyhow!("unable to determine config directory"))?
.join("notnow")
};
let config_dir = canonicalize_non_strict(&config_dir)
.with_context(|| format!("failed to canonicalize path `{}`", config_dir.display()))?;
let mut config_dir_rel = config_dir.components();
let _root = config_dir_rel.next();
let config_dir_rel = config_dir_rel.as_path();
debug_assert!(config_dir_rel.is_relative(), "{config_dir_rel:?}");
let state_dir = cache_dir()
.ok_or_else(|| anyhow!("unable to determine cache directory"))?
.join("notnow")
.join(config_dir_rel);
let slf = Self {
config_dir,
state_dir,
};
Ok(slf)
}
pub fn ui_config_dir(&self) -> &Path {
&self.config_dir
}
pub fn ui_config_file(&self) -> &OsStr {
OsStr::new("notnow.json")
}
pub fn tasks_dir(&self) -> PathBuf {
self.ui_config_dir().join("tasks")
}
pub fn ui_state_dir(&self) -> &Path {
&self.state_dir
}
pub fn ui_state_file(&self) -> &OsStr {
OsStr::new("ui-state.json")
}
pub(crate) fn lock_file(&self) -> PathBuf {
self.state_dir.join("notnow.lock")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn path_normalization() {
assert_eq!(normalize(Path::new("tmp/foobar/..")), Path::new("tmp"));
assert_eq!(normalize(Path::new("/tmp/foobar/..")), Path::new("/tmp"));
assert_eq!(normalize(Path::new("/tmp/.")), Path::new("/tmp"));
assert_eq!(normalize(Path::new("/tmp/./blah")), Path::new("/tmp/blah"));
assert_eq!(normalize(Path::new("/tmp/../blah")), Path::new("/blah"));
assert_eq!(normalize(Path::new("./foo")), Path::new("foo"));
assert_eq!(
normalize(Path::new("./foo/")).as_os_str(),
Path::new("foo").as_os_str()
);
assert_eq!(normalize(Path::new("foo")), Path::new("foo"));
assert_eq!(
normalize(Path::new("foo/")).as_os_str(),
Path::new("foo").as_os_str()
);
assert_eq!(normalize(Path::new("../foo")), Path::new("../foo"));
assert_eq!(normalize(Path::new("../foo/")), Path::new("../foo"));
assert_eq!(
normalize(Path::new("./././relative-dir-that-does-not-exist/../file")),
Path::new("file")
);
}
#[test]
fn non_strict_canonicalization() {
let dir = current_dir().unwrap();
let path = Path::new("relative-path-that-does-not-exist");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join(path));
let dir = current_dir().unwrap();
let path = Path::new("relative-path-that-does-not-exist/");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(
real.as_os_str(),
dir
.join(Path::new("relative-path-that-does-not-exist"))
.as_os_str()
);
let path = Path::new("relative-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join(path));
let path = Path::new("./relative-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join(normalize(path)));
let path = Path::new("./././relative-dir-that-does-not-exist/../file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join("file"));
let path = Path::new("../relative-path-that-does-not-exist");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(
real,
dir
.parent()
.unwrap()
.join("relative-path-that-does-not-exist")
);
let path = Path::new("../relative-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(
real,
dir
.parent()
.unwrap()
.join("relative-dir-that-does-not-exist/file")
);
let path = Path::new("/absolute-path-that-does-not-exist");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, path);
let path = Path::new("/absolute-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, path);
let dir = TempDir::new().unwrap();
let dir = dir.path();
let path = dir;
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, path);
let path = dir.join("foobar");
let real = canonicalize_non_strict(&path).unwrap();
assert_eq!(real, path);
}
#[test]
fn paths_instantiation() {
let _paths = Paths::new(None).unwrap();
let dir = TempDir::new().unwrap();
let path = dir.path().join("i").join("do").join("not").join("exist");
let _paths = Paths::new(Some(path)).unwrap();
}
}