Skip to main content

buntrail_core/contracts/
workspace.rs

1use super::{CoreError, ErrorCode};
2macro_rules! uuid_ids { ($($name:ident),*) => { $(#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct $name(pub [u8;16]);)* }; }
3uuid_ids!(
4    WorkspaceID,
5    DocumentID,
6    OperationID,
7    TerminalSessionID,
8    TerminalTabID,
9    ClientID,
10    WorkspaceLeaseID,
11    TransferID
12);
13#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub struct RequestId(pub u64);
15#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub struct ViewID(pub u64);
17#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
18pub enum RouteKind {
19    Local,
20    Ssh,
21    LocalContainer,
22    SshContainer,
23}
24#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
25pub enum ContainerEngine {
26    Docker,
27    Podman,
28}
29#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
30pub struct EnvironmentLocator {
31    pub route: RouteKind,
32    pub ssh_profile: Option<String>,
33    pub engine: Option<ContainerEngine>,
34    pub engine_context: Option<String>,
35    pub container_selector: Option<String>,
36    pub effective_user: Option<String>,
37}
38#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
39pub struct WorkspaceLocator {
40    pub environment: EnvironmentLocator,
41    pub root: TargetPath,
42}
43#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
44pub struct ContainerIdentity {
45    pub engine_endpoint: Vec<u8>,
46    pub immutable_id: Vec<u8>,
47}
48#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
49pub struct TargetIdentity {
50    pub security_binding: Vec<u8>,
51    pub effective_user: Vec<u8>,
52    pub filesystem_namespace: Vec<u8>,
53    pub container: Option<ContainerIdentity>,
54}
55#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
56pub struct RuntimeGeneration {
57    pub supervisor_boot: [u8; 16],
58    pub container_start: Option<Vec<u8>>,
59}
60#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
61pub struct WorkspaceLease {
62    pub client: ClientID,
63    pub workspace: WorkspaceID,
64    pub lease: WorkspaceLeaseID,
65    pub runtime: RuntimeGeneration,
66}
67#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
68pub enum TargetPath {
69    PosixBytes(Vec<u8>),
70    WindowsWide(Vec<u16>),
71}
72impl TargetPath {
73    /// Absolute filesystem paths only; preserve native units, including unpaired surrogates.
74    pub fn windows(units: Vec<u16>) -> Result<Self, CoreError> {
75        fn ascii_eq(units: &[u16], ascii: &str) -> bool {
76            units.len() == ascii.len()
77                && units.iter().zip(ascii.bytes()).all(|(u, b)| {
78                    *u == u16::from(b.to_ascii_uppercase())
79                        || *u == u16::from(b.to_ascii_lowercase())
80                })
81        }
82        fn trimmed(mut units: &[u16]) -> &[u16] {
83            while units.last().is_some_and(|u| *u == 32 || *u == 46) {
84                units = &units[..units.len() - 1];
85            }
86            units
87        }
88        fn dos_device(component: &[u16]) -> bool {
89            let base = trimmed(
90                component
91                    .split(|u| *u == 46 || *u == 58)
92                    .next()
93                    .unwrap_or_default(),
94            );
95            ["CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$"]
96                .iter()
97                .any(|name| ascii_eq(base, name))
98                || (base.len() == 4
99                    && (ascii_eq(&base[..3], "COM") || ascii_eq(&base[..3], "LPT"))
100                    && ((49..=57).contains(&base[3]) || [0xb9, 0xb2, 0xb3].contains(&base[3])))
101        }
102        let separator = |c: &u16| *c == 92 || *c == 47;
103        let drive = |p: &[u16]| {
104            p.len() >= 3
105                && ((65..=90).contains(&p[0]) || (97..=122).contains(&p[0]))
106                && p[1] == 58
107                && separator(&p[2])
108        };
109        let unc = |p: &[u16]| {
110            let mut parts = p.split(separator);
111            let server = parts.next().unwrap_or_default();
112            let share = parts.next().unwrap_or_default();
113            !server.is_empty()
114                && !share.is_empty()
115                && ![&[46][..], &[46, 46][..], &[63][..]].contains(&server)
116                && ![&[46][..], &[46, 46][..]].contains(&share)
117                && !ascii_eq(trimmed(share), "pipe")
118                && !ascii_eq(trimmed(share), "IPC$")
119        };
120        let absolute = if let Some(p) = units.strip_prefix(&[92, 92, 63, 92]) {
121            !p.contains(&47)
122                && (drive(p)
123                    || (p.len() >= 4
124                        && p[..3]
125                            .iter()
126                            .zip([85, 78, 67])
127                            .all(|(c, upper)| *c == upper || *c == upper + 32)
128                        && p[3] == 92
129                        && unc(&p[4..])))
130        } else {
131            drive(&units)
132                || (units.len() >= 2
133                    && separator(&units[0])
134                    && separator(&units[1])
135                    && unc(&units[2..]))
136        };
137        // Verbatim disk paths bypass DOS alias normalization and may name literal files.
138        let device = !units.starts_with(&[92, 92, 63, 92])
139            && if drive(&units) {
140                units[3..].split(separator).any(dos_device)
141            } else {
142                units
143                    .get(2..)
144                    .unwrap_or_default()
145                    .split(separator)
146                    .skip(2)
147                    .any(dos_device)
148            };
149        if units.contains(&0) || !absolute || device {
150            return Err(CoreError::new(
151                ErrorCode::InvalidArgument,
152                "expected absolute NUL-free Windows filesystem path",
153            ));
154        }
155        Ok(Self::WindowsWide(units))
156    }
157
158    pub fn posix(bytes: Vec<u8>) -> Result<Self, CoreError> {
159        if bytes.first() != Some(&b'/') || bytes.contains(&0) {
160            return Err(CoreError::new(
161                ErrorCode::InvalidArgument,
162                "expected absolute NUL-free POSIX path",
163            ));
164        }
165        Ok(Self::PosixBytes(bytes))
166    }
167    pub fn display(&self) -> String {
168        match self {
169            Self::PosixBytes(v) => String::from_utf8_lossy(v).into_owned(),
170            Self::WindowsWide(v) => String::from_utf16_lossy(v),
171        }
172    }
173}
174#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
175pub enum Capability {
176    Files,
177    Watch,
178    Search,
179    Git,
180    Terminal,
181    Operations,
182    Leases,
183    Language,
184}
185#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
186pub struct Capabilities(pub Vec<Capability>);
187#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
188pub enum ConnectionState {
189    Resolving,
190    Authenticating,
191    PreparingHelper,
192    Negotiating,
193    Ready,
194    Reconnecting,
195    Disconnected,
196    Failed(CoreError),
197    TargetChanged {
198        expected: TargetIdentity,
199        observed: TargetIdentity,
200    },
201}
202#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
203pub enum ConnectionCloseReason {
204    WindowDetachment,
205    ResolvedFinalClose,
206    ApplicationShutdown,
207}
208#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
209pub enum LeaseReleaseIntent {
210    ViewDetachment,
211    ResolvedWorkspaceClosure,
212}
213
214/// A read-only observation never registers or resumes the owner.
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub enum LeasePresence {
217    Active,
218    Detached { remaining_millis: u64 },
219    Absent,
220    RuntimeLost,
221}
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct LeaseObservation {
224    pub owner: WorkspaceLease,
225    pub target: TargetIdentity,
226    pub observed_runtime: RuntimeGeneration,
227    pub presence: LeasePresence,
228    pub workers_retired: bool,
229}