agent-migrate-core 0.6.1

Core models and transactional filesystem engine for agent configuration migration
Documentation
use std::fmt;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct BundleRelativePath(PathBuf);

impl BundleRelativePath {
    #[must_use]
    pub fn as_path(&self) -> &Path {
        &self.0
    }
}

impl FromStr for BundleRelativePath {
    type Err = BundlePathError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value.is_empty() || value.contains(['\0', '\\']) {
            return Err(BundlePathError::Invalid(value.to_owned()));
        }
        let path = PathBuf::from(value);
        let all_normal = path
            .components()
            .all(|component| matches!(component, Component::Normal(_)));
        let windows_prefix = value
            .split('/')
            .next()
            .is_some_and(|segment| segment.contains(':'));
        if !all_normal || windows_prefix {
            return Err(BundlePathError::Invalid(value.to_owned()));
        }
        Ok(Self(path))
    }
}

impl TryFrom<String> for BundleRelativePath {
    type Error = BundlePathError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value)
    }
}

impl From<BundleRelativePath> for String {
    fn from(value: BundleRelativePath) -> Self {
        value.to_string()
    }
}

impl fmt::Display for BundleRelativePath {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.0.to_string_lossy())
    }
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum BundlePathError {
    #[error("invalid bundle-relative path: {0}")]
    Invalid(String),
}