Skip to main content

buntrail_protocol/
services.rs

1//! Target-scoped DTOs. Paths are lossless base64; target authority comes only from Context.
2use crate::types::{WireVersion, decode_path};
3use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
4use buntrail_core::contracts as c;
5use serde::{Deserialize, Serialize};
6
7pub fn invalid() -> c::CoreError {
8    c::CoreError::new(c::ErrorCode::InvalidArgument, "Invalid service wire value")
9}
10pub fn path(p: &c::TargetPath) -> Result<String, c::CoreError> {
11    match p {
12        c::TargetPath::PosixBytes(b) => {
13            c::TargetPath::posix(b.clone())?;
14            Ok(B64.encode(b))
15        }
16        _ => Err(invalid()),
17    }
18}
19pub fn target_path(p: &str) -> Result<c::TargetPath, c::CoreError> {
20    c::TargetPath::posix(decode_path(p).map_err(|_| invalid())?)
21}
22pub fn uuid(b: [u8; 16]) -> String {
23    let s: String = b.iter().map(|b| format!("{b:02x}")).collect();
24    format!(
25        "{}-{}-{}-{}-{}",
26        &s[..8],
27        &s[8..12],
28        &s[12..16],
29        &s[16..20],
30        &s[20..]
31    )
32}
33pub fn id(s: &str) -> Result<[u8; 16], c::CoreError> {
34    if !crate::types::uuid(s) {
35        return Err(invalid());
36    }
37    let s = s.replace('-', "");
38    let mut b = [0; 16];
39    for (i, v) in b.iter_mut().enumerate() {
40        *v = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| invalid())?;
41    }
42    Ok(b)
43}
44pub fn hex(b: &[u8]) -> String {
45    b.iter().map(|b| format!("{b:02x}")).collect()
46}
47pub fn digest(s: &str) -> Result<[u8; 32], c::CoreError> {
48    if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
49        return Err(invalid());
50    }
51    let mut b = [0; 32];
52    for (i, v) in b.iter_mut().enumerate() {
53        *v = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| invalid())?;
54    }
55    Ok(b)
56}
57pub fn version(v: &c::FileVersion) -> WireVersion {
58    WireVersion {
59        opaque_token: B64.encode(&v.opaque_token),
60        content_digest: hex(&v.content_digest),
61        file_identity: B64.encode(&v.file_identity),
62    }
63}
64impl WireVersion {
65    pub fn core(&self, t: &c::TargetIdentity) -> Result<c::FileVersion, c::CoreError> {
66        Ok(c::FileVersion {
67            target: t.clone(),
68            opaque_token: B64.decode(&self.opaque_token).map_err(|_| invalid())?,
69            content_digest: digest(&self.content_digest)?,
70            file_identity: B64.decode(&self.file_identity).map_err(|_| invalid())?,
71        })
72    }
73}
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct File {
77    pub path_base64: String,
78    #[serde(deserialize_with = "crate::types::required_option")]
79    pub resolved_identity: Option<String>,
80}
81impl File {
82    pub fn from_core(f: &c::FileRef) -> Result<Self, c::CoreError> {
83        Ok(Self {
84            path_base64: path(&f.path)?,
85            resolved_identity: f.resolved_identity.as_ref().map(|b| B64.encode(b)),
86        })
87    }
88    pub fn core(&self, t: &c::TargetIdentity) -> Result<c::FileRef, c::CoreError> {
89        Ok(c::FileRef {
90            target: t.clone(),
91            path: target_path(&self.path_base64)?,
92            resolved_identity: self
93                .resolved_identity
94                .as_ref()
95                .map(|s| B64.decode(s).map_err(|_| invalid()))
96                .transpose()?,
97        })
98    }
99}
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct Worktree {
103    pub path_base64: String,
104    pub common_directory_base64: String,
105}
106impl Worktree {
107    pub fn from_core(w: &c::WorktreeIdentity) -> Result<Self, c::CoreError> {
108        Ok(Self {
109            path_base64: path(&w.canonical_path)?,
110            common_directory_base64: B64.encode(&w.common_git_directory_identity),
111        })
112    }
113    pub fn core(&self, t: &c::TargetIdentity) -> Result<c::WorktreeIdentity, c::CoreError> {
114        Ok(c::WorktreeIdentity {
115            target: t.clone(),
116            canonical_path: target_path(&self.path_base64)?,
117            common_git_directory_identity: B64
118                .decode(&self.common_directory_base64)
119                .map_err(|_| invalid())?,
120        })
121    }
122}
123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
125pub enum Head {
126    Branch { name_base64: String, oid: String },
127    Detached { oid: String },
128    Unborn { name_base64: String },
129}
130impl Head {
131    fn from_core(h: &c::GitHead) -> Self {
132        match h {
133            c::GitHead::Branch { name, oid } => Self::Branch {
134                name_base64: B64.encode(name),
135                oid: oid.clone(),
136            },
137            c::GitHead::Detached(oid) => Self::Detached { oid: oid.clone() },
138            c::GitHead::Unborn(name) => Self::Unborn {
139                name_base64: B64.encode(name),
140            },
141        }
142    }
143    fn core(&self) -> Result<c::GitHead, c::CoreError> {
144        Ok(match self {
145            Self::Branch { name_base64, oid } => c::GitHead::Branch {
146                name: B64.decode(name_base64).map_err(|_| invalid())?,
147                oid: oid.clone(),
148            },
149            Self::Detached { oid } => c::GitHead::Detached(oid.clone()),
150            Self::Unborn { name_base64 } => {
151                c::GitHead::Unborn(B64.decode(name_base64).map_err(|_| invalid())?)
152            }
153        })
154    }
155}
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct GitState {
159    pub head: Head,
160    pub index_digest: String,
161    pub worktree_digest: String,
162}
163impl GitState {
164    pub fn from_core(s: &c::GitExpectedState) -> Self {
165        Self {
166            head: Head::from_core(&s.head),
167            index_digest: hex(&s.index_fingerprint),
168            worktree_digest: hex(&s.worktree_fingerprint),
169        }
170    }
171    pub fn core(&self) -> Result<c::GitExpectedState, c::CoreError> {
172        Ok(c::GitExpectedState {
173            head: self.head.core()?,
174            index_fingerprint: digest(&self.index_digest)?,
175            worktree_fingerprint: digest(&self.worktree_digest)?,
176        })
177    }
178}
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
181pub enum GitCommand {
182    Repository { action: c::RepositoryAction },
183    Stage { paths: Vec<String> },
184    Unstage { paths: Vec<String> },
185    Commit { message: String },
186    Checkout { reference: String },
187    Fetch { remote: String },
188    Push { remote: String, reference: String },
189    PullFastForward { remote: String, reference: String },
190}
191impl GitCommand {
192    pub fn from_core(c: &c::GitMutation) -> Result<Self, c::CoreError> {
193        Ok(match c {
194            c::GitMutation::Repository(action) => Self::Repository {
195                action: action.clone(),
196            },
197            c::GitMutation::Stage(p) => Self::Stage {
198                paths: p.iter().map(path).collect::<Result<_, _>>()?,
199            },
200            c::GitMutation::Unstage(p) => Self::Unstage {
201                paths: p.iter().map(path).collect::<Result<_, _>>()?,
202            },
203            c::GitMutation::Commit { message } => Self::Commit {
204                message: message.clone(),
205            },
206            c::GitMutation::Checkout { reference } => Self::Checkout {
207                reference: B64.encode(reference),
208            },
209            c::GitMutation::Fetch { remote } => Self::Fetch {
210                remote: B64.encode(remote),
211            },
212            c::GitMutation::Push { remote, reference } => Self::Push {
213                remote: B64.encode(remote),
214                reference: B64.encode(reference),
215            },
216            c::GitMutation::PullFastForward { remote, reference } => Self::PullFastForward {
217                remote: B64.encode(remote),
218                reference: B64.encode(reference),
219            },
220        })
221    }
222    pub fn core(&self) -> Result<c::GitMutation, c::CoreError> {
223        let b = |s: &str| B64.decode(s).map_err(|_| invalid());
224        Ok(match self {
225            Self::Repository { action } => c::GitMutation::Repository(action.clone()),
226            Self::Stage { paths } => c::GitMutation::Stage(
227                paths
228                    .iter()
229                    .map(|p| target_path(p))
230                    .collect::<Result<_, _>>()?,
231            ),
232            Self::Unstage { paths } => c::GitMutation::Unstage(
233                paths
234                    .iter()
235                    .map(|p| target_path(p))
236                    .collect::<Result<_, _>>()?,
237            ),
238            Self::Commit { message } => c::GitMutation::Commit {
239                message: message.clone(),
240            },
241            Self::Checkout { reference } => c::GitMutation::Checkout {
242                reference: b(reference)?,
243            },
244            Self::Fetch { remote } => c::GitMutation::Fetch { remote: b(remote)? },
245            Self::Push { remote, reference } => c::GitMutation::Push {
246                remote: b(remote)?,
247                reference: b(reference)?,
248            },
249            Self::PullFastForward { remote, reference } => c::GitMutation::PullFastForward {
250                remote: b(remote)?,
251                reference: b(reference)?,
252            },
253        })
254    }
255}
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
258pub enum FileCommand {
259    CreateDirectory,
260    Rename {
261        destination: String,
262    },
263    Move {
264        destination: String,
265    },
266    Remove {
267        confirmed_target: String,
268        confirmed_path: String,
269    },
270    Delete {
271        confirmed_target: String,
272        confirmed_path: String,
273    },
274}
275#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(deny_unknown_fields)]
277pub struct Search {
278    pub root: File,
279    pub expression: String,
280    pub regex: bool,
281    pub case_sensitive: bool,
282    pub whole_word: bool,
283    pub includes: Vec<String>,
284    pub excludes: Vec<String>,
285    pub hidden: bool,
286    pub ignored: bool,
287}
288impl Search {
289    pub fn from_core(r: &c::SearchRequest) -> Result<Self, c::CoreError> {
290        Ok(Self {
291            root: File::from_core(&r.root)?,
292            expression: r.expression.clone(),
293            regex: matches!(r.mode, c::SearchMode::Regex),
294            case_sensitive: r.case_sensitive,
295            whole_word: r.whole_word,
296            includes: r.includes.clone(),
297            excludes: r.excludes.clone(),
298            hidden: r.hidden,
299            ignored: r.ignored,
300        })
301    }
302    pub fn core(&self, t: &c::TargetIdentity, id: u64) -> Result<c::SearchRequest, c::CoreError> {
303        Ok(c::SearchRequest {
304            request: c::RequestId(id),
305            root: self.root.core(t)?,
306            expression: self.expression.clone(),
307            mode: if self.regex {
308                c::SearchMode::Regex
309            } else {
310                c::SearchMode::Literal
311            },
312            case_sensitive: self.case_sensitive,
313            whole_word: self.whole_word,
314            includes: self.includes.clone(),
315            excludes: self.excludes.clone(),
316            hidden: self.hidden,
317            ignored: self.ignored,
318        })
319    }
320}
321#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(deny_unknown_fields)]
323pub struct LeaseOwner {
324    pub client_id: String,
325    pub workspace_id: String,
326    pub lease_id: String,
327    pub target_binding: String,
328    pub runtime_generation: String,
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub container_start_base64: Option<String>,
331}
332impl LeaseOwner {
333    pub fn from_core(
334        owner: &c::WorkspaceLease,
335        target: &c::TargetIdentity,
336    ) -> Result<Self, c::CoreError> {
337        use sha2::Digest;
338        if owner
339            .runtime
340            .container_start
341            .as_ref()
342            .is_some_and(|s| s.is_empty() || s.len() > 256)
343        {
344            return Err(invalid());
345        }
346        Ok(Self {
347            client_id: uuid(owner.client.0),
348            workspace_id: uuid(owner.workspace.0),
349            lease_id: uuid(owner.lease.0),
350            target_binding: hex(&sha2::Sha256::digest(
351                serde_json::to_vec(target).map_err(|_| invalid())?,
352            )),
353            runtime_generation: uuid(owner.runtime.supervisor_boot),
354            container_start_base64: owner
355                .runtime
356                .container_start
357                .as_ref()
358                .map(|s| B64.encode(s)),
359        })
360    }
361    pub fn context(&self) -> crate::types::Context {
362        crate::types::Context {
363            client_id: self.client_id.clone(),
364            workspace_id: self.workspace_id.clone(),
365            target_binding: self.target_binding.clone(),
366            runtime_generation: self.runtime_generation.clone(),
367        }
368    }
369    pub fn validate(&self) -> Result<(), c::CoreError> {
370        id(&self.client_id)?;
371        id(&self.workspace_id)?;
372        id(&self.lease_id)?;
373        id(&self.runtime_generation)?;
374        digest(&self.target_binding)?;
375        self.container_start()?;
376        Ok(())
377    }
378    pub fn container_start(&self) -> Result<Option<Vec<u8>>, c::CoreError> {
379        crate::types::decode_container_start(self.container_start_base64.as_deref())
380            .map_err(|_| invalid())
381    }
382}
383#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
385pub enum OwnerPresence {
386    Active {},
387    Detached { remaining_millis: u64 },
388    Absent {},
389    RuntimeLost {},
390}
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(tag = "method", rename_all = "snake_case", deny_unknown_fields)]
393pub enum Call {
394    Repository {
395        worktree: Worktree,
396    },
397    Commit {
398        worktree: Worktree,
399        oid: String,
400        cursor: Option<u64>,
401    },
402    FilteredHistory {
403        worktree: Worktree,
404        refs: Vec<String>,
405        cursor: Option<String>,
406        filter: c::HistoryFilter,
407    },
408    LeaseObserve {
409        owner: LeaseOwner,
410    },
411    FileResolve {
412        root: File,
413        purpose: ResolvePurpose,
414    },
415    TerminalOpen {
416        lease_id: String,
417        session_id: String,
418        directory_base64: String,
419        shell_base64: String,
420        columns: u16,
421        rows: u16,
422    },
423    TerminalAttach {
424        lease_id: String,
425        session_id: String,
426        last_output_sequence: u64,
427    },
428    TerminalInput {
429        lease_id: String,
430        session_id: String,
431    },
432    TerminalResize {
433        lease_id: String,
434        session_id: String,
435        columns: u16,
436        rows: u16,
437    },
438    TerminalEnd {
439        lease_id: String,
440        session_id: String,
441        intent: crate::terminal::EndIntent,
442    },
443    List {
444        file: File,
445    },
446    PathVersion {
447        file: File,
448    },
449    Watch {
450        file: File,
451    },
452    Search {
453        request: Search,
454    },
455    FileMutate {
456        file: File,
457        #[serde(deserialize_with = "crate::types::required_option")]
458        expected: Option<WireVersion>,
459        command: FileCommand,
460    },
461    Discover {
462        file: File,
463    },
464    Contains {
465        worktree: Worktree,
466        file: File,
467    },
468    GitRead {
469        worktree: Worktree,
470        query: c::GitRead,
471    },
472    Status {
473        worktree: Worktree,
474    },
475    Diff {
476        worktree: Worktree,
477        path_base64: String,
478        source: DiffSource,
479    },
480    History {
481        worktree: Worktree,
482        refs: Vec<String>,
483        #[serde(deserialize_with = "crate::types::required_option")]
484        cursor: Option<String>,
485    },
486    GitMutate {
487        worktree: Worktree,
488        expected: GitState,
489        command: GitCommand,
490    },
491    GitExisting {
492        worktree: Worktree,
493        expected: GitState,
494        command: GitCommand,
495    },
496    Inspect,
497}
498impl Call {
499    pub fn requires_repository(&self) -> bool {
500        matches!(
501            self,
502            Self::Repository { .. }
503                | Self::Commit { .. }
504                | Self::FilteredHistory { .. }
505                | Self::GitMutate {
506                    command: GitCommand::Repository { .. },
507                    ..
508                }
509                | Self::GitExisting {
510                    command: GitCommand::Repository { .. },
511                    ..
512                }
513                | Self::Diff {
514                    source: DiffSource::Commit { .. },
515                    ..
516                }
517        )
518    }
519
520    pub fn terminal_lease(&self) -> Option<&str> {
521        match self {
522            Self::TerminalOpen { lease_id, .. }
523            | Self::TerminalAttach { lease_id, .. }
524            | Self::TerminalInput { lease_id, .. }
525            | Self::TerminalResize { lease_id, .. }
526            | Self::TerminalEnd { lease_id, .. } => Some(lease_id),
527            _ => None,
528        }
529    }
530    pub fn effectful(&self) -> bool {
531        self.mutating()
532            || matches!(
533                self,
534                Self::TerminalInput { .. } | Self::TerminalResize { .. } | Self::TerminalEnd { .. }
535            )
536    }
537
538    pub fn unprepared_worktree_mutation(&self) -> bool {
539        match self {
540            Self::GitMutate { command, .. } => command.core().is_ok_and(|v| v.changes_worktree()),
541            _ => false,
542        }
543    }
544
545    pub fn mutating(&self) -> bool {
546        matches!(
547            self,
548            Self::FileMutate { .. } | Self::GitMutate { .. } | Self::TerminalOpen { .. }
549        )
550    }
551}
552#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
554pub enum DiffSource {
555    Index,
556    Worktree,
557    Commit { oid: String },
558}
559impl DiffSource {
560    pub fn from_core(s: &c::DiffSource) -> Self {
561        match s {
562            c::DiffSource::Index => Self::Index,
563            c::DiffSource::Worktree => Self::Worktree,
564            c::DiffSource::Commit(oid) => Self::Commit { oid: oid.clone() },
565        }
566    }
567    pub fn core(&self) -> c::DiffSource {
568        match self {
569            Self::Index => c::DiffSource::Index,
570            Self::Worktree => c::DiffSource::Worktree,
571            Self::Commit { oid } => c::DiffSource::Commit(oid.clone()),
572        }
573    }
574}
575#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
576#[serde(deny_unknown_fields)]
577pub struct Entry {
578    pub file: File,
579    pub display_name: String,
580    pub kind: String,
581}
582#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
583#[serde(deny_unknown_fields)]
584pub struct GitEntry {
585    pub path_base64: String,
586    #[serde(deserialize_with = "crate::types::required_option")]
587    pub previous_path: Option<String>,
588    pub status: String,
589}
590impl GitEntry {
591    pub fn from_core(e: &c::GitEntry) -> Result<Self, c::CoreError> {
592        Ok(Self {
593            path_base64: path(&e.path)?,
594            previous_path: e.previous_path.as_ref().map(path).transpose()?,
595            status: e.status.clone(),
596        })
597    }
598    pub fn core(&self) -> Result<c::GitEntry, c::CoreError> {
599        Ok(c::GitEntry {
600            path: target_path(&self.path_base64)?,
601            previous_path: self.previous_path.as_deref().map(target_path).transpose()?,
602            status: self.status.clone(),
603        })
604    }
605}
606#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
607#[serde(deny_unknown_fields)]
608pub struct Commit {
609    pub lane: u32,
610    pub parent_lanes: Vec<u32>,
611    pub author: String,
612    pub authored_at: String,
613    pub message: String,
614    pub oid: String,
615    pub parents: Vec<String>,
616    pub decorations: Vec<String>,
617    pub subject: String,
618}
619impl Commit {
620    pub fn from_core(c: &c::GraphCommit) -> Self {
621        Self {
622            lane: c.lane,
623            parent_lanes: c.parent_lanes.clone(),
624            author: c.author.clone(),
625            authored_at: c.authored_at.clone(),
626            message: c.message.clone(),
627            oid: c.oid.clone(),
628            parents: c.parents.clone(),
629            decorations: c.decorations.iter().map(|b| B64.encode(b)).collect(),
630            subject: c.subject.clone(),
631        }
632    }
633    pub fn core(&self) -> Result<c::GraphCommit, c::CoreError> {
634        Ok(c::GraphCommit {
635            lane: self.lane,
636            parent_lanes: self.parent_lanes.clone(),
637            author: self.author.clone(),
638            authored_at: self.authored_at.clone(),
639            message: self.message.clone(),
640            oid: self.oid.clone(),
641            parents: self.parents.clone(),
642            decorations: self
643                .decorations
644                .iter()
645                .map(|b| B64.decode(b).map_err(|_| invalid()))
646                .collect::<Result<_, _>>()?,
647            subject: self.subject.clone(),
648        })
649    }
650}
651#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
652#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
653pub enum Reply {
654    Repository {
655        snapshot: Box<c::GitRepositorySnapshot>,
656    },
657    Commit {
658        page: c::CommitPage,
659    },
660    GitRead {
661        stdout_base64: String,
662        stderr_base64: String,
663        success: bool,
664    },
665    LeaseObserved {
666        owner: LeaseOwner,
667        runtime_generation: String,
668        presence: OwnerPresence,
669        workers_retired: bool,
670    },
671    FileResolved {
672        root: File,
673        file: File,
674        resolved_path: String,
675        #[serde(deserialize_with = "crate::types::required_option")]
676        node_kind: Option<String>,
677        #[serde(deserialize_with = "crate::types::required_option")]
678        version: Option<WireVersion>,
679    },
680    TerminalAck,
681    Terminal {
682        event: crate::terminal::Event,
683    },
684    List {
685        entries: Vec<Entry>,
686    },
687    Version {
688        version: WireVersion,
689    },
690    Watch {
691        file: File,
692        event: String,
693    },
694    SearchMatch {
695        file: File,
696        line: u64,
697        column_utf8: u64,
698        preview: String,
699        version: WireVersion,
700        digest: String,
701    },
702    SearchProgress {
703        files: u64,
704        matches: u64,
705    },
706    SearchComplete,
707    SearchCancelled,
708    SearchLimited {
709        maximum: u64,
710    },
711    Discovered {
712        #[serde(deserialize_with = "crate::types::required_option")]
713        worktree: Option<Worktree>,
714    },
715    Contains {
716        value: bool,
717    },
718    Status {
719        worktree: Worktree,
720        state: GitState,
721        staged: Vec<GitEntry>,
722        unstaged: Vec<GitEntry>,
723        untracked: Vec<GitEntry>,
724        conflicts: Vec<GitEntry>,
725    },
726    Diff {
727        source: DiffSource,
728        content: String,
729        #[serde(deserialize_with = "crate::types::required_option")]
730        unavailable: Option<String>,
731        change: String,
732        #[serde(deserialize_with = "crate::types::required_option")]
733        previous_path: Option<String>,
734        expected: GitState,
735        bytes_base64: String,
736    },
737    History {
738        commits: Vec<Commit>,
739        continuation_edges: Vec<(String, String)>,
740        #[serde(deserialize_with = "crate::types::required_option")]
741        next_cursor: Option<String>,
742    },
743    Receipt {
744        receipt: Receipt,
745    },
746    GitExisting {
747        #[serde(deserialize_with = "crate::types::required_option")]
748        receipt: Option<Receipt>,
749    },
750    Error {
751        code: String,
752        message: String,
753    },
754}
755impl Reply {
756    pub fn error(e: c::CoreError) -> Self {
757        Self::Error {
758            code: format!("{:?}", e.code),
759            message: e.message,
760        }
761    }
762    pub fn search(e: c::SearchEvent) -> Result<Self, c::CoreError> {
763        Ok(match e {
764            c::SearchEvent::Match(m) => Self::SearchMatch {
765                file: File::from_core(&m.file)?,
766                line: m.line,
767                column_utf8: m.preview_column_utf8,
768                preview: m.preview,
769                version: version(&m.searched_version),
770                digest: hex(&m.searched_digest),
771            },
772            c::SearchEvent::Progress { files, matches } => Self::SearchProgress { files, matches },
773            c::SearchEvent::Complete => Self::SearchComplete,
774            c::SearchEvent::Cancelled => Self::SearchCancelled,
775            c::SearchEvent::Limited { maximum } => Self::SearchLimited { maximum },
776            c::SearchEvent::Failed(e) => Self::error(e),
777        })
778    }
779}
780#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
781#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
782pub enum Observation {
783    Unobserved,
784    File {
785        file: File,
786        #[serde(deserialize_with = "crate::types::required_option")]
787        version: Option<WireVersion>,
788        #[serde(deserialize_with = "crate::types::required_option")]
789        digest: Option<String>,
790    },
791    Git {
792        worktree: Worktree,
793        state: GitState,
794    },
795    GitNetwork {
796        worktree: Worktree,
797        state: GitState,
798        intent: Box<NetworkIntent>,
799        #[serde(deserialize_with = "crate::types::required_option")]
800        observation: Option<NetworkObservation>,
801    },
802}
803#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
804#[serde(deny_unknown_fields)]
805pub struct NetworkIntent {
806    pub action: String,
807    pub remote: String,
808    pub reference: String,
809    #[serde(deserialize_with = "crate::types::required_option")]
810    pub endpoint_fingerprint: Option<String>,
811    pub push_mapping_unconfigured: bool,
812}
813#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct NetworkObservation {
816    #[serde(deserialize_with = "crate::types::required_option")]
817    pub remote_oid: Option<String>,
818    pub remote_refs: String,
819    pub local_refs: String,
820    pub fetch_head: String,
821    pub fetch_head_oids: String,
822}
823impl Observation {
824    pub fn from_core(o: &c::ObservedState) -> Result<Self, c::CoreError> {
825        Ok(match o {
826            c::ObservedState::Unobserved => Self::Unobserved,
827            c::ObservedState::File {
828                file,
829                version: v,
830                digest: d,
831            } => Self::File {
832                file: File::from_core(file)?,
833                version: v.as_ref().map(version),
834                digest: d.as_ref().map(|b| hex(b)),
835            },
836            c::ObservedState::Git { worktree, state } => Self::Git {
837                worktree: Worktree::from_core(worktree)?,
838                state: GitState::from_core(state),
839            },
840            c::ObservedState::GitNetwork {
841                worktree,
842                state,
843                intent,
844                observation,
845            } => Self::GitNetwork {
846                worktree: Worktree::from_core(worktree)?,
847                state: GitState::from_core(state),
848                intent: Box::new(NetworkIntent {
849                    action: format!("{:?}", intent.action),
850                    remote: B64.encode(&intent.remote),
851                    reference: B64.encode(&intent.reference),
852                    endpoint_fingerprint: intent.endpoint_fingerprint.as_ref().map(|b| hex(b)),
853                    push_mapping_unconfigured: intent.push_mapping_unconfigured,
854                }),
855                observation: observation.as_ref().map(|o| NetworkObservation {
856                    remote_oid: o.remote_oid.clone(),
857                    remote_refs: hex(&o.remote_refs_fingerprint),
858                    local_refs: hex(&o.local_refs_fingerprint),
859                    fetch_head: hex(&o.fetch_head_fingerprint),
860                    fetch_head_oids: B64.encode(&o.fetch_head_oids),
861                }),
862            },
863            _ => return Err(invalid()),
864        })
865    }
866    pub fn core(&self, t: &c::TargetIdentity) -> Result<c::ObservedState, c::CoreError> {
867        Ok(match self {
868            Self::Unobserved => c::ObservedState::Unobserved,
869            Self::File {
870                file,
871                version,
872                digest: d,
873            } => c::ObservedState::File {
874                file: file.core(t)?,
875                version: version.as_ref().map(|v| v.core(t)).transpose()?,
876                digest: d.as_deref().map(digest).transpose()?,
877            },
878            Self::Git { worktree, state } => c::ObservedState::Git {
879                worktree: worktree.core(t)?,
880                state: state.core()?,
881            },
882            Self::GitNetwork {
883                worktree,
884                state,
885                intent: i,
886                observation: o,
887            } => c::ObservedState::GitNetwork {
888                worktree: worktree.core(t)?,
889                state: state.core()?,
890                intent: c::GitNetworkIntent {
891                    action: match i.action.as_str() {
892                        "Fetch" => c::GitNetworkAction::Fetch,
893                        "Push" => c::GitNetworkAction::Push,
894                        "PullFastForward" => c::GitNetworkAction::PullFastForward,
895                        _ => return Err(invalid()),
896                    },
897                    remote: B64.decode(&i.remote).map_err(|_| invalid())?,
898                    reference: B64.decode(&i.reference).map_err(|_| invalid())?,
899                    endpoint_fingerprint: i
900                        .endpoint_fingerprint
901                        .as_deref()
902                        .map(digest)
903                        .transpose()?,
904                    push_mapping_unconfigured: i.push_mapping_unconfigured,
905                },
906                observation: o
907                    .as_ref()
908                    .map(|o| {
909                        Ok(c::GitNetworkObservation {
910                            remote_oid: o.remote_oid.clone(),
911                            remote_refs_fingerprint: digest(&o.remote_refs)?,
912                            local_refs_fingerprint: digest(&o.local_refs)?,
913                            fetch_head_fingerprint: digest(&o.fetch_head)?,
914                            fetch_head_oids: B64
915                                .decode(&o.fetch_head_oids)
916                                .map_err(|_| invalid())?,
917                        })
918                    })
919                    .transpose()?,
920            },
921        })
922    }
923}
924#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
925#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
926pub enum ResultValue {
927    TerminalOpened {
928        session_id: String,
929        runtime_generation: String,
930        #[serde(deserialize_with = "crate::types::required_option")]
931        container_start_base64: Option<String>,
932    },
933    Saved {
934        revision: u64,
935        version: WireVersion,
936    },
937    FileChanged {
938        file: File,
939        #[serde(deserialize_with = "crate::types::required_option")]
940        version: Option<WireVersion>,
941    },
942    GitChanged {
943        worktree: Worktree,
944        state: GitState,
945        #[serde(default, skip_serializing_if = "Option::is_none")]
946        outcome: Option<Box<c::RepositoryOutcome>>,
947    },
948}
949#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
950#[serde(deny_unknown_fields)]
951pub struct Receipt {
952    pub operation_id: String,
953    pub state: String,
954    #[serde(deserialize_with = "crate::types::required_option")]
955    pub result: Option<ResultValue>,
956    #[serde(deserialize_with = "crate::types::required_option")]
957    pub code: Option<String>,
958    pub message: String,
959    pub possible_effects: bool,
960    pub observed: Box<Observation>,
961}
962impl Receipt {
963    pub fn from_core(r: &c::OperationReceipt) -> Result<Self, c::CoreError> {
964        let mut w = Self {
965            operation_id: uuid(r.id.0),
966            state: String::new(),
967            result: None,
968            code: None,
969            message: String::new(),
970            possible_effects: false,
971            observed: Box::new(Observation::Unobserved),
972        };
973        w.state = match &r.state {
974            c::OperationState::Accepted => "accepted",
975            c::OperationState::Running => "running",
976            c::OperationState::Confirmed(r) => {
977                w.result = Some(match r {
978                    c::MutationResult::Saved(s) => ResultValue::Saved {
979                        revision: s.saved_revision.0,
980                        version: version(&s.version),
981                    },
982                    c::MutationResult::FileChanged { file, version: v } => {
983                        ResultValue::FileChanged {
984                            file: File::from_core(file)?,
985                            version: v.as_ref().map(version),
986                        }
987                    }
988                    c::MutationResult::GitChanged {
989                        worktree,
990                        state,
991                        outcome,
992                    } => ResultValue::GitChanged {
993                        outcome: outcome.clone(),
994                        worktree: Worktree::from_core(worktree)?,
995                        state: GitState::from_core(state),
996                    },
997                    c::MutationResult::TerminalOpened { session, runtime } => {
998                        ResultValue::TerminalOpened {
999                            session_id: uuid(session.0),
1000                            runtime_generation: uuid(runtime.supervisor_boot),
1001                            container_start_base64: runtime
1002                                .container_start
1003                                .as_ref()
1004                                .map(|s| B64.encode(s)),
1005                        }
1006                    }
1007                });
1008                "confirmed"
1009            }
1010            c::OperationState::Failed(f) => {
1011                w.code = Some(format!("{:?}", f.error.code));
1012                w.message = f.error.message.clone();
1013                w.possible_effects = f.effect == c::FailureEffect::PossiblePartialEffects;
1014                w.observed = Box::new(Observation::from_core(&f.observed)?);
1015                "failed"
1016            }
1017            c::OperationState::Unknown(e) => {
1018                w.message = e.explanation.clone();
1019                w.possible_effects = true;
1020                w.observed = Box::new(Observation::from_core(&e.observed)?);
1021                "unknown"
1022            }
1023        }
1024        .into();
1025        Ok(w)
1026    }
1027    pub fn core(&self, t: &c::TargetIdentity) -> Result<c::OperationReceipt, c::CoreError> {
1028        Ok(c::OperationReceipt {
1029            id: c::OperationID(id(&self.operation_id)?),
1030            state: match self.state.as_str() {
1031                "accepted" => c::OperationState::Accepted,
1032                "running" => c::OperationState::Running,
1033                "confirmed" => c::OperationState::Confirmed(
1034                    match self.result.as_ref().ok_or_else(invalid)? {
1035                        ResultValue::TerminalOpened {
1036                            session_id,
1037                            runtime_generation,
1038                            container_start_base64,
1039                        } => c::MutationResult::TerminalOpened {
1040                            session: c::TerminalSessionID(id(session_id)?),
1041                            runtime: c::RuntimeGeneration {
1042                                supervisor_boot: id(runtime_generation)?,
1043                                container_start: container_start_base64
1044                                    .as_ref()
1045                                    .map(|s| B64.decode(s).map_err(|_| invalid()))
1046                                    .transpose()?,
1047                            },
1048                        },
1049                        ResultValue::Saved { revision, version } => {
1050                            c::MutationResult::Saved(c::SaveResult {
1051                                saved_revision: c::BufferRevision(*revision),
1052                                version: version.core(t)?,
1053                            })
1054                        }
1055                        ResultValue::FileChanged { file, version } => {
1056                            c::MutationResult::FileChanged {
1057                                file: file.core(t)?,
1058                                version: version.as_ref().map(|v| v.core(t)).transpose()?,
1059                            }
1060                        }
1061                        ResultValue::GitChanged {
1062                            worktree,
1063                            state,
1064                            outcome,
1065                        } => c::MutationResult::GitChanged {
1066                            outcome: outcome.clone(),
1067                            worktree: worktree.core(t)?,
1068                            state: state.core()?,
1069                        },
1070                    },
1071                ),
1072                "failed" => c::OperationState::Failed(c::OperationFailure {
1073                    error: core_error(self.code.as_deref().ok_or_else(invalid)?, &self.message),
1074                    target: t.clone(),
1075                    observed: self.observed.core(t)?,
1076                    effect: if self.possible_effects {
1077                        c::FailureEffect::PossiblePartialEffects
1078                    } else {
1079                        c::FailureEffect::ProvenNoEffect
1080                    },
1081                }),
1082                "unknown" => c::OperationState::Unknown(c::ReconciliationEvidence {
1083                    target: t.clone(),
1084                    observed: self.observed.core(t)?,
1085                    explanation: self.message.clone(),
1086                }),
1087                _ => return Err(invalid()),
1088            },
1089        })
1090    }
1091}
1092pub fn core_error(code: &str, message: &str) -> c::CoreError {
1093    use c::ErrorCode::*;
1094    c::CoreError::new(
1095        match code {
1096            "InvalidArgument" | "invalid_argument" => InvalidArgument,
1097            "StaleRevision" => StaleRevision,
1098            "Unavailable" | "unavailable" => Unavailable,
1099            "Busy" | "busy" => Busy,
1100            "Cancelled" => Cancelled,
1101            "Unknown" => Unknown,
1102            "TargetChanged" => TargetChanged,
1103            "Closed" => Closed,
1104            "Io" => Io,
1105            _ => Unknown,
1106        },
1107        message,
1108    )
1109}
1110
1111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1112#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1113pub enum ResolvePurpose {
1114    Root,
1115    OpenChild { path_base64: String },
1116    MutationNode { path_base64: String },
1117}
1118impl ResolvePurpose {
1119    pub fn from_core(value: &c::FileResolvePurpose) -> Result<Self, c::CoreError> {
1120        Ok(match value {
1121            c::FileResolvePurpose::Root => Self::Root,
1122            c::FileResolvePurpose::OpenChild(p) => Self::OpenChild {
1123                path_base64: path(p)?,
1124            },
1125            c::FileResolvePurpose::MutationNode(p) => Self::MutationNode {
1126                path_base64: path(p)?,
1127            },
1128        })
1129    }
1130    pub fn core(&self) -> Result<c::FileResolvePurpose, c::CoreError> {
1131        Ok(match self {
1132            Self::Root => c::FileResolvePurpose::Root,
1133            Self::OpenChild { path_base64 } => {
1134                c::FileResolvePurpose::OpenChild(target_path(path_base64)?)
1135            }
1136            Self::MutationNode { path_base64 } => {
1137                c::FileResolvePurpose::MutationNode(target_path(path_base64)?)
1138            }
1139        })
1140    }
1141}