Skip to main content

strop_engine/
files.rs

1//! Application open targets (0042). `FileTarget` is the *unresolved* input an
2//! open/browse command was given — a local path or a remote URI spelling — as
3//! opposed to `strop_workspace::ResourceLocation`, the resolved identity of a
4//! resource once it is known. Remote URIs and native local paths stay distinct.
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7use strop_workspace::{AddressError, RemoteLocation};
8
9/// Local paths keep their existing native-byte wire representation; remote targets
10/// have an explicit remote envelope, never an ambiguous legacy local-path string.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum FileTarget {
13    Local(PathBuf),
14    Remote(RemoteLocation),
15    /// A path inside a running container (0037 DC1b) — the path names
16    /// container bytes; there is never a local interpretation.
17    Container {
18        container: strop_workspace::ContainerId,
19        path: PathBuf,
20    },
21}
22
23impl Serialize for FileTarget {
24    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
25        #[derive(Serialize)]
26        struct RemoteRecord<'a> {
27            remote: &'a RemoteLocation,
28        }
29        #[derive(Serialize)]
30        struct ContainerRecord<'a> {
31            container: &'a strop_workspace::ContainerId,
32            #[serde(with = "strop_core::path_serde")]
33            path: &'a PathBuf,
34        }
35        match self {
36            Self::Local(path) => strop_core::path_serde::serialize(path, serializer),
37            Self::Remote(remote) => RemoteRecord { remote }.serialize(serializer),
38            Self::Container { container, path } => {
39                ContainerRecord { container, path }.serialize(serializer)
40            }
41        }
42    }
43}
44impl<'de> Deserialize<'de> for FileTarget {
45    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
46        #[derive(Deserialize)]
47        #[serde(untagged)]
48        enum Record {
49            Remote {
50                remote: RemoteLocation,
51            },
52            Container {
53                container: strop_workspace::ContainerId,
54                #[serde(with = "strop_core::path_serde")]
55                path: PathBuf,
56            },
57            Local(#[serde(with = "strop_core::path_serde")] PathBuf),
58        }
59        Ok(match Record::deserialize(deserializer)? {
60            Record::Remote { remote } => Self::Remote(remote),
61            Record::Container { container, path } => Self::Container { container, path },
62            Record::Local(path) => Self::Local(path),
63        })
64    }
65}
66impl FileTarget {
67    pub fn resource_location(&self) -> Option<strop_workspace::ResourceLocation> {
68        use strop_workspace::{Filesystem, ResourceLocation};
69        match self {
70            Self::Local(path) if path.is_absolute() => Some(ResourceLocation::local(path.clone())),
71            Self::Remote(location) => location.absolute_file().map(|file| {
72                ResourceLocation::remote(file.endpoint().clone(), file.path().to_path_buf())
73            }),
74            Self::Container { container, path } if path.is_absolute() => Some(ResourceLocation {
75                filesystem: Filesystem::Container(container.clone()),
76                path: path.clone(),
77            }),
78            _ => None,
79        }
80    }
81    pub fn from_location(
82        location: &strop_workspace::ResourceLocation,
83    ) -> Result<Self, AddressError> {
84        use strop_workspace::Filesystem;
85        if !location.path.is_absolute() {
86            return Err(AddressError::RelativePath);
87        }
88        Ok(match &location.filesystem {
89            Filesystem::Local => Self::Local(location.path.clone()),
90            Filesystem::Remote(endpoint) => Self::Remote(
91                strop_workspace::RemoteFile::from_path(endpoint.clone(), location.path.clone())?
92                    .into(),
93            ),
94            Filesystem::Container(container) => Self::Container {
95                container: container.clone(),
96                path: location.path.clone(),
97            },
98        })
99    }
100
101    pub fn matches_location(&self, location: &strop_workspace::ResourceLocation) -> bool {
102        use strop_workspace::Filesystem;
103        match (self, &location.filesystem) {
104            (Self::Local(path), Filesystem::Local) => path == &location.path,
105            (Self::Remote(remote), Filesystem::Remote(endpoint)) => remote
106                .absolute_file()
107                .is_some_and(|file| file.endpoint() == endpoint && file.path() == location.path),
108            (Self::Container { container, path }, Filesystem::Container(expected)) => {
109                container == expected && path == &location.path
110            }
111            _ => false,
112        }
113    }
114    /// Only textual user-entry boundaries interpret the scheme. Filesystem/LSP
115    /// callers construct Local directly, including filenames containing `ssh:`.
116    pub fn parse(value: PathBuf) -> Result<Self, AddressError> {
117        if let Some(text) = value.to_str() {
118            if text.starts_with("ssh://") {
119                return RemoteLocation::parse(text).map(Self::Remote);
120            }
121            if text.starts_with("file://") || text.starts_with("container:") {
122                return strop_workspace::ResourceLocation::parse_uri(text)
123                    .and_then(|location| Self::from_location(&location));
124            }
125        }
126        Ok(Self::Local(value))
127    }
128    pub fn local_path(&self) -> Option<&Path> {
129        match self {
130            Self::Local(path) => Some(path),
131            _ => None,
132        }
133    }
134}
135impl std::fmt::Display for FileTarget {
136    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Self::Local(path) => path.display().fmt(formatter),
139            Self::Remote(file) => file.fmt(formatter),
140            Self::Container { container, path } => write!(
141                formatter,
142                "container:{}{}",
143                &container.as_str()[..12],
144                path.display()
145            ),
146        }
147    }
148}