use crate::config::parsers::TryFromKv;
use crate::secrets::SecretError;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String")]
#[serde(rename_all = "kebab-case")]
pub struct AbsolutePath(PathBuf);
impl TryFrom<String> for AbsolutePath {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl AbsolutePath {
pub fn into_inner(self) -> PathBuf {
self.0
}
pub fn new(path: impl AsRef<Path>) -> Self {
Self(path.as_ref().absolute())
}
pub fn canonicalize(&self) -> Result<CanonicalPath, SecretError> {
CanonicalPath::try_new(&self.0)
}
pub fn as_path(&self) -> &Path {
&self.0
}
pub fn parent(&self) -> Option<AbsolutePath> {
self.0.parent().map(AbsolutePath::new)
}
pub fn join(&self, path: impl AsRef<Path>) -> Self {
Self::new(self.0.join(path))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(try_from = "String")]
pub struct CanonicalPath(PathBuf);
impl TryFrom<String> for CanonicalPath {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl CanonicalPath {
pub fn into_inner(self) -> PathBuf {
self.0
}
pub fn try_new(path: impl AsRef<Path>) -> Result<Self, SecretError> {
Ok(Self(path.as_ref().canon()?))
}
pub fn as_path(&self) -> &Path {
&self.0
}
pub fn join(&self, path: impl AsRef<Path>) -> AbsolutePath {
AbsolutePath::new(self.0.join(path))
}
pub fn parent(&self) -> Option<AbsolutePath> {
self.0.parent().map(AbsolutePath::new)
}
}
impl From<CanonicalPath> for AbsolutePath {
fn from(canon: CanonicalPath) -> Self {
Self(canon.0)
}
}
impl From<PathBuf> for AbsolutePath {
fn from(p: PathBuf) -> Self {
Self::new(p)
}
}
impl From<&Path> for AbsolutePath {
fn from(p: &Path) -> Self {
Self::new(p)
}
}
impl From<&PathBuf> for AbsolutePath {
fn from(p: &PathBuf) -> Self {
Self::new(p)
}
}
impl TryFrom<PathBuf> for CanonicalPath {
type Error = SecretError;
fn try_from(p: PathBuf) -> Result<Self, Self::Error> {
CanonicalPath::try_new(&p)
}
}
impl TryFrom<&Path> for CanonicalPath {
type Error = SecretError;
fn try_from(p: &Path) -> Result<Self, Self::Error> {
CanonicalPath::try_new(p)
}
}
impl TryFrom<&PathBuf> for CanonicalPath {
type Error = SecretError;
fn try_from(p: &PathBuf) -> Result<Self, Self::Error> {
CanonicalPath::try_new(p)
}
}
trait PathExt {
fn clean(&self) -> PathBuf;
fn absolute(&self) -> PathBuf;
fn canon(&self) -> Result<PathBuf, SecretError>;
}
impl PathExt for Path {
fn clean(&self) -> PathBuf {
let mut components = self.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
fn absolute(&self) -> PathBuf {
let anchored = std::path::absolute(self).unwrap_or_else(|_| self.to_path_buf());
anchored.clean()
}
fn canon(&self) -> Result<PathBuf, SecretError> {
self.canonicalize().map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => SecretError::SourceMissing(self.to_path_buf()),
_ => SecretError::Io(e),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathMapping {
#[serde(alias = "source")]
src: CanonicalPath,
#[serde(alias = "dest")]
dst: AbsolutePath,
}
impl TryFromKv for PathMapping {
type Err = SecretError;
fn try_from_kv(key: String, val: String) -> Result<Self, SecretError> {
PathMapping::try_new(CanonicalPath::try_new(key)?, AbsolutePath::new(val))
}
}
impl PathMapping {
pub fn try_new(src: CanonicalPath, dst: AbsolutePath) -> Result<Self, SecretError> {
Ok(Self { src, dst })
}
pub fn src(&self) -> &CanonicalPath {
&self.src
}
pub fn dst(&self) -> &AbsolutePath {
&self.dst
}
}
impl FromStr for PathMapping {
type Err = String;
fn from_str(s: &str) -> Result<PathMapping, String> {
let (src, dst) = s
.split_once(':')
.or_else(|| s.split_once('='))
.ok_or_else(|| {
format!(
"Invalid mapping format '{}'. Expected SRC:DST or SRC=DST",
s
)
})?;
PathMapping::try_new(CanonicalPath::from_str(src)?, AbsolutePath::from_str(dst)?)
.map_err(|e| format!("Failed to create PathMapping '{}': {}", src, e))
}
}
impl Deref for AbsolutePath {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for AbsolutePath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl std::fmt::Display for AbsolutePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.display().fmt(f)
}
}
impl FromStr for AbsolutePath {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(AbsolutePath(Path::new(s).absolute()))
}
}
impl Deref for CanonicalPath {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for CanonicalPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl std::fmt::Display for CanonicalPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.display().fmt(f)
}
}
impl FromStr for CanonicalPath {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
CanonicalPath::try_new(Path::new(s)).map_err(|e| e.to_string())
}
}
impl PartialEq<Path> for AbsolutePath {
fn eq(&self, other: &Path) -> bool {
self.0 == other
}
}
impl PartialEq<PathBuf> for AbsolutePath {
fn eq(&self, other: &PathBuf) -> bool {
self.0 == *other
}
}
impl PartialEq<AbsolutePath> for Path {
fn eq(&self, other: &AbsolutePath) -> bool {
self == other.0
}
}
impl PartialEq<AbsolutePath> for PathBuf {
fn eq(&self, other: &AbsolutePath) -> bool {
*self == other.0
}
}
impl PartialEq<Path> for CanonicalPath {
fn eq(&self, other: &Path) -> bool {
self.0 == other
}
}
impl PartialEq<PathBuf> for CanonicalPath {
fn eq(&self, other: &PathBuf) -> bool {
self.0 == *other
}
}
impl PartialEq<CanonicalPath> for Path {
fn eq(&self, other: &CanonicalPath) -> bool {
self == other.0
}
}
impl PartialEq<CanonicalPath> for PathBuf {
fn eq(&self, other: &CanonicalPath) -> bool {
*self == other.0
}
}
impl std::borrow::Borrow<Path> for AbsolutePath {
fn borrow(&self) -> &Path {
&self.0
}
}
impl std::borrow::Borrow<Path> for CanonicalPath {
fn borrow(&self) -> &Path {
&self.0
}
}
pub fn parse_secret_path(s: &str) -> Result<crate::secrets::Secret, String> {
crate::secrets::Secret::from_file(s).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_basic_clean() {
assert_eq!(Path::new("a/b/c").clean(), PathBuf::from("a/b/c"));
assert_eq!(Path::new("a/./b/./c").clean(), PathBuf::from("a/b/c"));
}
#[test]
fn test_trailing_slashes() {
assert_eq!(Path::new("a/b/").clean(), PathBuf::from("a/b"));
assert_eq!(
Path::new("/tmp/secret/").clean(),
PathBuf::from("/tmp/secret")
);
assert_eq!(
Path::new("secret.yaml/").clean(),
PathBuf::from("secret.yaml")
);
}
#[test]
fn test_parent_dir_absolute() {
assert_eq!(Path::new("/a/b/../c").clean(), PathBuf::from("/a/c"));
assert_eq!(Path::new("/a/b/../../c").clean(), PathBuf::from("/c"));
}
#[test]
fn test_root_boundary() {
assert_eq!(Path::new("/..").clean(), PathBuf::from("/"));
assert_eq!(Path::new("/../a").clean(), PathBuf::from("/a"));
}
#[test]
fn test_complex() {
assert_eq!(
Path::new("./a/b/../../c/./d/").clean(),
PathBuf::from("c/d")
);
}
#[test]
fn test_absolute_path_cleaning() {
let p = AbsolutePath::new("a/b/../c");
let s = p.to_string();
assert!(!s.contains(".."), "Path should be cleaned of '..'");
assert!(s.ends_with("c"), "Path should end with 'c'");
}
#[test]
fn test_canonical_path_must_exist() {
let tmp = tempdir().unwrap();
let file_path = tmp.path().join("config.yaml");
let res = CanonicalPath::try_new(&file_path);
assert!(matches!(res, Err(SecretError::SourceMissing(_))));
std::fs::write(&file_path, "content").unwrap();
let res = CanonicalPath::try_new(&file_path);
assert!(res.is_ok());
let link_path = tmp.path().join("link");
#[cfg(unix)]
std::os::unix::fs::symlink(&file_path, &link_path).unwrap();
#[cfg(unix)]
{
let canon = CanonicalPath::try_new(&link_path).unwrap();
assert_eq!(canon.into_inner(), file_path.canonicalize().unwrap());
}
}
#[test]
fn test_mapping_parse() {
let tmp = tempdir().unwrap();
let src = tmp.path().join("src");
std::fs::write(&src, "").unwrap();
let src_str = src.to_str().unwrap();
let s = format!("{}:/dst", src_str);
let m = PathMapping::from_str(&s).expect("should parse valid mapping");
assert_eq!(m.src(), src.canonicalize().unwrap().as_path());
assert_eq!(m.dst(), Path::new("/dst"));
assert!(PathMapping::from_str("garbage").is_err());
let s_missing = format!("{}_missing:/dst", src_str);
assert!(PathMapping::from_str(&s_missing).is_err());
}
}