use std::fmt;
use std::path::{Path, PathBuf};
use crate::error::{Error, Reason, Result};
use crate::join::{clean, join_impl};
use crate::platform::Layout;
use crate::{ops, os, rules};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Scope {
User,
Global,
Portable,
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::User => "user",
Self::Global => "global",
Self::Portable => "portable",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind {
Data,
Config,
Cache,
Logs,
State,
Runtime,
Temp,
Documents,
}
impl Kind {
pub const ALL: [Self; 8] = [
Self::Data,
Self::Config,
Self::Cache,
Self::Logs,
Self::State,
Self::Runtime,
Self::Temp,
Self::Documents,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Data => "data",
Self::Config => "config",
Self::Cache => "cache",
Self::Logs => "logs",
Self::State => "state",
Self::Runtime => "runtime",
Self::Temp => "temp",
Self::Documents => "documents",
}
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct App {
name: Box<str>,
scope: Scope,
layout: Layout,
}
macro_rules! kind_accessors {
($($kind:ident => $get:ident, $at:ident, $what:literal;)*) => {
$(
#[doc = concat!("The ", $what, " directory. See [`Kind::", stringify!($kind), "`].")]
#[must_use]
#[inline]
pub fn $get(&self) -> &Path {
&self.layout.$get
}
#[doc = concat!("A hardened sub-path inside the ", $what, " directory.")]
#[inline]
pub fn $at(&self, sub: impl AsRef<str>) -> Result<PathBuf> {
join_impl(&self.layout.$get, sub.as_ref())
}
)*
};
}
impl App {
pub fn new(name: impl AsRef<str>) -> Result<Self> {
let name = validate_name(name.as_ref())?;
let layout = os::user_bases()?.layout(name);
Ok(Self::build(name, Scope::User, layout))
}
pub fn global(name: impl AsRef<str>) -> Result<Self> {
let name = validate_name(name.as_ref())?;
let layout = os::system_bases()?.layout(name);
Ok(Self::build(name, Scope::Global, layout))
}
pub fn portable(name: impl AsRef<str>, root: impl AsRef<Path>) -> Result<Self> {
let name = validate_name(name.as_ref())?;
let root = root.as_ref();
if root.as_os_str().is_empty() {
return Err(Error::invalid_path("", Reason::Empty));
}
let root = if root.is_absolute() {
clean(root)
} else {
let cwd = std::env::current_dir()
.map_err(|e| Error::io("read current directory", root, e))?;
clean(cwd.join(root))
};
Ok(Self::build(name, Scope::Portable, Layout::under(&root)))
}
fn build(name: &str, scope: Scope, layout: Layout) -> Self {
Self {
name: name.into(),
scope,
layout,
}
}
#[must_use]
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
#[inline]
pub fn scope(&self) -> Scope {
self.scope
}
#[must_use]
#[inline]
pub fn dir(&self, kind: Kind) -> &Path {
match kind {
Kind::Data => &self.layout.data,
Kind::Config => &self.layout.config,
Kind::Cache => &self.layout.cache,
Kind::Logs => &self.layout.logs,
Kind::State => &self.layout.state,
Kind::Runtime => &self.layout.runtime,
Kind::Temp => &self.layout.temp,
Kind::Documents => &self.layout.documents,
}
}
#[inline]
pub fn at(&self, kind: Kind, sub: impl AsRef<str>) -> Result<PathBuf> {
join_impl(self.dir(kind), sub.as_ref())
}
kind_accessors! {
Data => data, data_at, "persistent data";
Config => config, config_at, "configuration";
Cache => cache, cache_at, "cache";
Logs => logs, logs_at, "log";
State => state, state_at, "state";
Runtime => runtime, runtime_at, "runtime";
Temp => temp, temp_at, "private temporary";
Documents => documents, documents_at, "user-visible documents";
}
pub fn ensure(&self, kind: Kind) -> Result<&Path> {
let dir = self.dir(kind);
ops::create_dir(dir, Some(self.dir_mode()))?;
Ok(dir)
}
pub fn ensure_all(&self) -> Result<()> {
for kind in Kind::ALL {
let _dir = self.ensure(kind)?;
}
Ok(())
}
pub fn create(&self, kind: Kind, sub: impl AsRef<str>) -> Result<PathBuf> {
let path = self.at(kind, sub)?;
ops::create_dir(&path, Some(self.dir_mode()))?;
Ok(path)
}
pub fn list(&self, kind: Kind, sub: impl AsRef<str>) -> Result<Vec<PathBuf>> {
ops::list(self.at(kind, sub)?)
}
pub fn remove(&self, kind: Kind, sub: impl AsRef<str>) -> Result<bool> {
ops::remove(self.at(kind, sub)?)
}
pub fn find(&self, kind: Kind, pattern: impl AsRef<str>) -> Result<Vec<PathBuf>> {
ops::find(self.dir(kind), pattern)
}
fn dir_mode(&self) -> u32 {
match self.scope {
Scope::Global => 0o755,
Scope::User | Scope::Portable => 0o700,
}
}
}
impl fmt::Display for App {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{} ({} scope)", self.name, self.scope)?;
for (i, kind) in Kind::ALL.iter().enumerate() {
write!(f, " {:<9} {}", kind.name(), self.dir(*kind).display())?;
if i + 1 < Kind::ALL.len() {
f.write_str("\n")?;
}
}
Ok(())
}
}
fn validate_name(name: &str) -> Result<&str> {
rules::check_segment(name, false).map_err(|r| Error::invalid_name(name, r))?;
if name.starts_with('~') {
return Err(Error::invalid_name(name, Reason::HomeShortcut));
}
Ok(name)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_validate_name_accepts_ordinary_names() {
for n in ["HiveDB", "hive-db", "Hive DB", "app_1", "名前", ".hidden"] {
assert!(validate_name(n).is_ok(), "{n:?}");
}
}
#[test]
fn test_validate_name_rejects_unsafe_names() {
let cases = [
("", Reason::Empty),
("a/b", Reason::Separator),
("a\\b", Reason::Separator),
("..", Reason::TrailingDotOrSpace),
("C:", Reason::ReservedChar(':')),
("CON", Reason::ReservedName),
("~app", Reason::HomeShortcut),
("a\0", Reason::ControlChar),
];
for (name, want) in cases {
let err = validate_name(name).unwrap_err();
assert!(matches!(err, Error::InvalidName { .. }), "{name:?}");
assert_eq!(err.reason(), Some(want), "{name:?}");
}
}
#[test]
fn test_kind_all_is_complete_and_named() {
assert_eq!(Kind::ALL.len(), 8);
for k in Kind::ALL {
assert_eq!(k.to_string(), k.name());
}
}
#[test]
fn test_portable_layout_and_accessors_agree() {
let root = std::env::temp_dir().join("path-lib-unit-portable");
let app = App::portable("Unit", &root).unwrap();
assert_eq!(app.scope(), Scope::Portable);
assert_eq!(app.name(), "Unit");
for kind in Kind::ALL {
assert!(app.dir(kind).starts_with(clean(&root)));
assert_eq!(app.at(kind, "").unwrap(), app.dir(kind));
}
assert_eq!(app.data(), app.dir(Kind::Data));
assert_eq!(app.logs_at("x").unwrap(), app.logs().join("x"));
}
#[test]
fn test_portable_rejects_empty_root() {
assert!(App::portable("a", "").is_err());
}
#[test]
fn test_display_lists_every_kind() {
let app = App::portable("Shown", std::env::temp_dir()).unwrap();
let text = app.to_string();
assert!(text.starts_with("Shown (portable scope)"));
for kind in Kind::ALL {
assert!(text.contains(kind.name()));
}
assert!(!text.ends_with('\n'));
}
}