use std::path::{Path, PathBuf}; /// A sandbox writable-path grant, paired with the canonical target it was /// resolved to when it was approved. /// /// Persisting the resolved canonical path (rather than re-resolving the /// requested path by string at enforcement time) closes a symlink TOCTOU hole: /// the path that is actually enforced is the one that was vetted at approval /// time. #[derive(Clone, Debug, PartialEq, Eq)] pub struct GrantedWritePath { /// The path exactly as the user/model requested it (untrusted; display/provenance). pub requested: PathBuf, /// The canonical, symlink-resolved target established when the grant was /// approved. `None` for a legacy/hand-authored bare-string settings entry, /// which is resolved fresh at enforcement time. pub resolved: Option, /// Windows/WSL only: whether the canonical target lives on a Windows-hosted /// (DrvFs) filesystem, whose sandbox-integrity guarantees are weaker than a /// distro-native filesystem. Recorded at approval time (after the user was /// shown the weaker-guarantee warning) so later commands can reuse the grant /// without re-classifying it. Always `false` on other platforms and for /// legacy/bare-string entries. pub on_windows_fs: bool, } impl GrantedWritePath { /// A grant from a bare requested path with no resolved canonical target /// (resolved fresh at enforcement time). pub fn from_requested(requested: PathBuf) -> Self { Self { requested, resolved: None, on_windows_fs: false, } } /// A grant whose canonical target was resolved when it was approved. Assumes /// a native (non-Windows-hosted) filesystem; use [`Self::resolved_on_fs`] to /// record the Windows/WSL DrvFs classification. pub fn resolved(requested: PathBuf, resolved: PathBuf) -> Self { Self::resolved_on_fs(requested, resolved, false) } /// A grant whose canonical target was resolved when it was approved, /// recording whether that target lives on a Windows-hosted (DrvFs) /// filesystem. pub fn resolved_on_fs(requested: PathBuf, resolved: PathBuf, on_windows_fs: bool) -> Self { Self { requested, resolved: Some(resolved), on_windows_fs, } } /// The path used for lexical subtree/coverage/dedup logic: the resolved /// canonical target when known (the real grant), otherwise the requested /// path. pub fn canonical_or_requested(&self) -> &Path { self.resolved.as_deref().unwrap_or(&self.requested) } } // Hand-written serde so grants round-trip with legacy persisted data: a bare // JSON string deserializes to `{ requested, resolved: None }`, and a `None` // resolved serializes back as a bare string. This mirrors the string-or-object // approach used by `GrantedWritePathContent` in `settings_content`. impl serde::Serialize for GrantedWritePath { fn serialize(&self, serializer: S) -> Result { match &self.resolved { None => self.requested.serialize(serializer), Some(resolved) => { use serde::ser::SerializeStruct as _; // Emit `on_windows_fs` only when set, so native grants (the // common case, and every grant on non-Windows platforms) keep // the compact two-field form and legacy rows round-trip. let field_count = if self.on_windows_fs { 3 } else { 2 }; let mut state = serializer.serialize_struct("GrantedWritePath", field_count)?; state.serialize_field("requested", &self.requested)?; state.serialize_field("resolved", resolved)?; if self.on_windows_fs { state.serialize_field("on_windows_fs", &self.on_windows_fs)?; } state.end() } } } } impl<'de> serde::Deserialize<'de> for GrantedWritePath { fn deserialize>(deserializer: D) -> Result { #[derive(serde::Deserialize)] struct Object { requested: PathBuf, #[serde(default)] resolved: Option, #[serde(default)] on_windows_fs: bool, } #[derive(serde::Deserialize)] #[serde(untagged)] enum StringOrObject { String(PathBuf), Object(Object), } Ok(match StringOrObject::deserialize(deserializer)? { StringOrObject::String(requested) => Self { requested, resolved: None, on_windows_fs: false, }, StringOrObject::Object(Object { requested, resolved, on_windows_fs, }) => Self { requested, resolved, on_windows_fs, }, }) } }