Skip to main content

cageforge_policy/
context.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Runtime inputs for resolving symbolic [`crate::PathSelector`] values.
4//!
5//! [`crate::PathResolutionContext`] is supplied by the harness or backend. It
6//! stores declarations only and never discovers a workspace or follows links;
7//! [`crate::FilesystemPolicy`] consumes it when evaluating a concrete path.
8
9use crate::PathSelector;
10use crate::PolicyError;
11use cageforge_path::NativePathKey;
12use std::collections::HashSet;
13use std::path::Path;
14use std::path::PathBuf;
15
16/// Runtime paths needed to resolve platform-independent policy selectors.
17///
18/// The context is supplied by a harness or a platform backend. Constructing it
19/// never reads the filesystem, follows symlinks, or infers a workspace.
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct PathResolutionContext {
22    root_paths: Vec<PathBuf>,
23    root_keys: HashSet<NativePathKey>,
24    workspace_roots: Vec<PathBuf>,
25    workspace_root_keys: HashSet<NativePathKey>,
26    minimal_paths: Vec<PathBuf>,
27    minimal_path_keys: HashSet<NativePathKey>,
28    executable_roots: Vec<PathBuf>,
29    executable_root_keys: HashSet<NativePathKey>,
30    tmpdir: Option<PathBuf>,
31    slash_tmp: Option<PathBuf>,
32    current_directory: Option<PathBuf>,
33}
34
35impl PathResolutionContext {
36    /// Creates an empty context.
37    pub fn new() -> Self {
38        Self {
39            root_paths: Vec::new(),
40            root_keys: HashSet::new(),
41            workspace_roots: Vec::new(),
42            workspace_root_keys: HashSet::new(),
43            minimal_paths: Vec::new(),
44            minimal_path_keys: HashSet::new(),
45            executable_roots: Vec::new(),
46            executable_root_keys: HashSet::new(),
47            tmpdir: None,
48            slash_tmp: None,
49            current_directory: None,
50        }
51    }
52
53    /// Adds one absolute system root represented by the runtime environment.
54    ///
55    /// POSIX backends normally provide `/`. Windows backends may provide more
56    /// than one drive or UNC root. The context never discovers these paths on
57    /// its own.
58    pub fn with_root(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
59        let path = validated_absolute(path.into())?;
60        if self.root_keys.insert(NativePathKey::new(&path)) {
61            self.root_paths.push(path);
62        }
63        Ok(self)
64    }
65
66    /// Adds one absolute workspace root.
67    pub fn with_workspace_root(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
68        let path = validated_absolute(path.into())?;
69        if self.workspace_root_keys.insert(NativePathKey::new(&path)) {
70            self.workspace_roots.push(path);
71        }
72        Ok(self)
73    }
74
75    /// Adds one absolute path required by ordinary process execution.
76    pub fn with_minimal_path(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
77        let path = validated_absolute(path.into())?;
78        if self.minimal_path_keys.insert(NativePathKey::new(&path)) {
79            self.minimal_paths.push(path);
80        }
81        Ok(self)
82    }
83
84    /// Adds one absolute runtime root whose executable files may be mapped by
85    /// a backend that supports this capability.
86    ///
87    /// This declaration is intentionally separate from readable filesystem
88    /// roots. Reading a runtime file does not by itself authorize the native
89    /// loader to map it executable.
90    pub fn with_executable_root(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
91        let path = validated_absolute(path.into())?;
92        if self.executable_root_keys.insert(NativePathKey::new(&path)) {
93            self.executable_roots.push(path);
94        }
95        Ok(self)
96    }
97
98    /// Sets the platform temporary directory.
99    pub fn with_tmpdir(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
100        self.tmpdir = Some(validated_absolute(path.into())?);
101        Ok(self)
102    }
103
104    /// Sets the conventional `/tmp` directory when the platform provides it.
105    pub fn with_slash_tmp(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
106        self.slash_tmp = Some(validated_absolute(path.into())?);
107        Ok(self)
108    }
109
110    /// Sets the absolute runtime current directory used for command cwd
111    /// resolution and for commands that otherwise inherit their cwd.
112    ///
113    /// This is runtime input only; the context never reads the directory or
114    /// changes the process cwd.
115    pub fn with_current_directory(mut self, path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
116        self.current_directory = Some(validated_absolute(path.into())?);
117        Ok(self)
118    }
119
120    /// Returns the configured workspace roots.
121    pub fn workspace_roots(&self) -> &[PathBuf] {
122        &self.workspace_roots
123    }
124
125    /// Returns the absolute system roots supplied by the runtime.
126    pub fn root_paths(&self) -> &[PathBuf] {
127        &self.root_paths
128    }
129
130    /// Returns the configured minimal runtime paths.
131    pub fn minimal_paths(&self) -> &[PathBuf] {
132        &self.minimal_paths
133    }
134
135    /// Returns runtime roots declared for native executable mapping.
136    pub fn executable_roots(&self) -> &[PathBuf] {
137        &self.executable_roots
138    }
139
140    /// Returns the configured platform temporary directory.
141    pub fn tmpdir(&self) -> Option<&Path> {
142        self.tmpdir.as_deref()
143    }
144
145    /// Returns the configured conventional `/tmp` directory.
146    pub fn slash_tmp(&self) -> Option<&Path> {
147        self.slash_tmp.as_deref()
148    }
149
150    /// Returns the absolute runtime directory used for relative command cwd
151    /// values and for commands that inherit their cwd.
152    pub fn current_directory(&self) -> Option<&Path> {
153        self.current_directory.as_deref()
154    }
155}
156
157fn validated_absolute(path: PathBuf) -> Result<PathBuf, PolicyError> {
158    PathSelector::absolute(path)?
159        .path()
160        .map(Path::to_path_buf)
161        .ok_or_else(|| PolicyError::InvalidContext {
162            message: "absolute path validation returned a non-absolute selector".to_string(),
163        })
164}