use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::Error;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DirName(String);
impl DirName {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for DirName {
type Error = Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
validate(&value)?;
Ok(DirName(value))
}
}
impl AsRef<str> for DirName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<Path> for DirName {
fn as_ref(&self) -> &Path {
Path::new(&self.0)
}
}
impl std::fmt::Display for DirName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
fn invalid(name: &str, detail: &str) -> Error {
Error::InvalidName {
name: name.to_string(),
detail: detail.to_string(),
}
}
fn validate(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(invalid(name, "empty"));
}
if name == "." || name == ".." {
return Err(invalid(name, "cannot be '.' or '..'"));
}
if name.contains('\0') {
return Err(invalid(name, "contains a NUL byte"));
}
if name.contains('/') {
return Err(invalid(name, "contains '/'"));
}
#[cfg(windows)]
{
if name
.chars()
.any(|c| matches!(c, '\\' | ':' | '<' | '>' | '"' | '|' | '?' | '*'))
{
return Err(invalid(
name,
"contains a character not allowed in a Windows directory name",
));
}
if name != name.trim_end_matches(['.', ' ']) {
return Err(invalid(
name,
"ends in a '.' or space, which Windows reserves",
));
}
if is_windows_reserved_device(name) {
return Err(invalid(name, "is a Windows reserved device name"));
}
}
Ok(())
}
#[cfg(windows)]
fn is_windows_reserved_device(name: &str) -> bool {
let trailing: String = name
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.collect();
let trailing: String = trailing.chars().rev().collect();
let base = &name[..name.len() - trailing.len()];
match base {
"CON" | "PRN" | "AUX" | "NUL" => true,
"COM" | "LPT" => !trailing.is_empty(),
_ => false,
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AppDir(String);
impl AppDir {
pub fn from_path(path: &Path) -> Self {
AppDir(path.to_string_lossy().into_owned())
}
pub fn as_path(&self) -> &Path {
Path::new(&self.0)
}
}
impl From<String> for AppDir {
fn from(value: String) -> Self {
AppDir(value)
}
}
impl From<&str> for AppDir {
fn from(value: &str) -> Self {
AppDir(value.to_owned())
}
}
impl AsRef<Path> for AppDir {
fn as_ref(&self) -> &Path {
self.as_path()
}
}
impl From<AppDir> for PathBuf {
fn from(value: AppDir) -> Self {
PathBuf::from(value.0)
}
}