Skip to main content

abrasive_protocol/
lib.rs

1mod errors;
2
3pub use errors::DecodeError;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize)]
8pub enum Message {
9    Manifest(Manifest),
10    NeedFiles(Vec<String>),
11    FileData { path: String, contents: Vec<u8> },
12    SyncDone,
13    SyncAck,
14    BuildStdout(Vec<u8>),
15    BuildStderr(Vec<u8>),
16    BuildFinished { exit_code: u8 },
17    /// Server-side rejection: all slots for this (team, scope) are
18    /// currently busy. The client should sleep and retry the whole
19    /// connection. Sent in place of NeedFiles.
20    SlotsBusy,
21    /// First message of every build attempt. Carries both a cheap
22    /// "is anything stale?" fingerprint and the full build request,
23    /// so the daemon can fast-path straight to cargo without waiting
24    /// for a separate BuildRequest message after the probe.
25    ///
26    /// Fingerprint is a hash of (path, mtime, size) for every file
27    /// in the workspace — no file contents read. The daemon caches
28    /// the last accepted fingerprint per (slot, team, scope) in
29    /// memory; on a hit it sends ProbeAccepted and starts cargo
30    /// immediately, on a miss it sends ProbeMiss and expects the
31    /// usual Manifest flow before running the embedded request.
32    Probe {
33        fingerprint: [u8; 32],
34        request: BuildRequest,
35    },
36    ProbeAccepted,
37    ProbeMiss,
38}
39
40impl Message {
41    /// Short, human-readable name of the variant — for error messages
42    /// that don't want to Debug-dump entire payloads.
43    pub fn kind(&self) -> &'static str {
44        match self {
45            Message::Manifest(_) => "Manifest",
46            Message::NeedFiles(_) => "NeedFiles",
47            Message::FileData { .. } => "FileData",
48            Message::SyncDone => "SyncDone",
49            Message::SyncAck => "SyncAck",
50            Message::BuildStdout(_) => "BuildStdout",
51            Message::BuildStderr(_) => "BuildStderr",
52            Message::BuildFinished { .. } => "BuildFinished",
53            Message::SlotsBusy => "SlotsBusy",
54            Message::Probe { .. } => "Probe",
55            Message::ProbeAccepted => "ProbeAccepted",
56            Message::ProbeMiss => "ProbeMiss",
57        }
58    }
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62pub struct Manifest {
63    pub team: String,
64    pub scope: String,
65    /// gzip(bincode(Vec<FileEntry>))
66    pub files_gz: Vec<u8>,
67}
68
69impl Manifest {
70    pub fn encode_files(files: &[FileEntry]) -> Vec<u8> {
71        use flate2::{Compression, write::GzEncoder};
72        use std::io::Write;
73        let raw = bincode::serialize(files).unwrap();
74        let mut enc = GzEncoder::new(Vec::new(), Compression::default());
75        enc.write_all(&raw).unwrap();
76        enc.finish().unwrap()
77    }
78
79    pub fn decode_files(&self) -> Result<Vec<FileEntry>, DecodeError> {
80        use flate2::read::GzDecoder;
81        use std::io::Read;
82        let mut dec = GzDecoder::new(&self.files_gz[..]);
83        let mut raw = Vec::new();
84        dec.read_to_end(&mut raw).map_err(|e| DecodeError(Box::new(bincode::ErrorKind::Custom(e.to_string()))))?;
85        bincode::deserialize(&raw).map_err(DecodeError)
86    }
87}
88
89#[derive(Debug, Serialize, Deserialize)]
90pub struct FileEntry {
91    pub path: String,
92    pub hash: [u8; 32],
93}
94
95/// Architecture
96#[derive(Debug, Serialize, Deserialize)]
97#[repr(u8)]
98pub enum Arch {
99    X86_64 = 0,
100    Aarch64 = 1,
101}
102
103/// Operating System
104#[derive(Debug, Serialize, Deserialize)]
105#[repr(u8)]
106pub enum Os {
107    Windows = 0,
108    Linux = 1,
109    Mac = 2,
110}
111
112/// Application Binary Interface
113#[derive(Debug, Serialize, Deserialize)]
114#[repr(u8)]
115pub enum Abi {
116    Gnu = 0,
117    Musl = 1,
118    Msvc = 2,
119}
120
121#[derive(Debug, Serialize, Deserialize)]
122pub struct PlatformTriple {
123    pub arch: Arch,
124    pub os: Os,
125    pub abi: Abi,
126}
127
128impl PlatformTriple {
129    pub fn as_cargo_target_string(&self) -> String {
130        match (&self.arch, &self.os, &self.abi) {
131            (Arch::X86_64, Os::Linux, Abi::Gnu) => "x86_64-unknown-linux-gnu",
132            (Arch::X86_64, Os::Linux, Abi::Musl) => "x86_64-unknown-linux-musl",
133            (Arch::Aarch64, Os::Linux, Abi::Gnu) => "aarch64-unknown-linux-gnu",
134            (Arch::Aarch64, Os::Linux, Abi::Musl) => "aarch64-unknown-linux-musl",
135            (Arch::X86_64, Os::Windows, Abi::Msvc) => "x86_64-pc-windows-msvc",
136            (Arch::X86_64, Os::Windows, Abi::Gnu) => "x86_64-pc-windows-gnu",
137            (Arch::Aarch64, Os::Windows, Abi::Msvc) => "aarch64-pc-windows-msvc",
138            (Arch::X86_64, Os::Mac, _) => "x86_64-apple-darwin",
139            (Arch::Aarch64, Os::Mac, _) => "aarch64-apple-darwin",
140            _ => unimplemented!(),
141        }
142        .to_string()
143    }
144}
145
146#[derive(Debug, Serialize, Deserialize)]
147pub struct BuildRequest {
148    pub cargo_args: Vec<String>,
149    pub subdir: Option<String>,
150    pub host_platform: PlatformTriple,
151    pub team: String,
152    pub scope: String,
153}
154
155/// Serialize a Message into a bincode payload. WebSocket framing handles
156/// length-prefixing for us, so this is just the raw bincode bytes.
157pub fn serialize(msg: &Message) -> Vec<u8> {
158    bincode::serialize(msg).unwrap()
159}
160
161/// Deserialize a Message from a bincode payload received over WebSockets.
162pub fn deserialize(raw: &[u8]) -> Result<Message, DecodeError> {
163    bincode::deserialize(raw).map_err(DecodeError)
164}