Skip to main content

agent_migrate_core/
path_mapping.rs

1use std::path::{Component, Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct PathMapping {
8    source: PathBuf,
9    target: PathBuf,
10}
11
12impl PathMapping {
13    /// Creates a normalized absolute path mapping.
14    ///
15    /// # Errors
16    ///
17    /// Returns an error if either side is not normalized and absolute.
18    pub fn new(source: PathBuf, target: PathBuf) -> Result<Self, PathMappingError> {
19        if !valid_absolute(&source) || !valid_absolute(&target) {
20            return Err(PathMappingError::Invalid);
21        }
22        Ok(Self { source, target })
23    }
24
25    #[must_use]
26    pub fn source(&self) -> &Path {
27        &self.source
28    }
29
30    #[must_use]
31    pub fn target(&self) -> &Path {
32        &self.target
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct PathMappingSet {
38    mappings: Vec<PathMapping>,
39}
40
41impl PathMappingSet {
42    /// Creates a deterministic longest-prefix mapping set.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error when two mappings declare the same source.
47    pub fn new(mut mappings: Vec<PathMapping>) -> Result<Self, PathMappingError> {
48        mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.source.components().count()));
49        for pair in mappings.windows(2) {
50            if pair[0].source == pair[1].source {
51                return Err(PathMappingError::DuplicateSource);
52            }
53        }
54        Ok(Self { mappings })
55    }
56
57    /// Maps an absolute path using component-boundary prefix matching.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error for invalid input or when no mapping applies.
62    pub fn map(&self, input: &Path) -> Result<PathBuf, PathMappingError> {
63        if !valid_absolute(input) {
64            return Err(PathMappingError::Invalid);
65        }
66        self.mappings
67            .iter()
68            .find_map(|mapping| {
69                input
70                    .strip_prefix(&mapping.source)
71                    .ok()
72                    .map(|suffix| mapping.target.join(suffix))
73            })
74            .ok_or(PathMappingError::Unmapped)
75    }
76}
77
78fn valid_absolute(path: &Path) -> bool {
79    path.is_absolute()
80        && path
81            .components()
82            .all(|component| !matches!(component, Component::ParentDir))
83}
84
85#[derive(Debug, Error, PartialEq, Eq)]
86pub enum PathMappingError {
87    #[error("path mapping must use normalized absolute paths")]
88    Invalid,
89    #[error("path mapping contains a duplicate source")]
90    DuplicateSource,
91    #[error("absolute path has no declared mapping")]
92    Unmapped,
93}