1use crate::codec::bytes;
3use crate::wire::tail::RECORD_LIMIT;
4use anyhow::{Result, ensure};
5const BASE32: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
6pub const MAX_MOVE_TASKS: usize = 256;
7fn valid(ok: bool) -> Result<()> {
8 ensure!(ok, "error encrypted-tail-invalid");
9 Ok(())
10}
11fn encode_task(bytes: &[u8]) -> String {
12 let mut value = 0_u128;
13 for byte in bytes {
14 value = (value << 8) | u128::from(*byte);
15 }
16 (0..16)
17 .rev()
18 .map(|shift| BASE32[((value >> (shift * 5)) & 31) as usize] as char)
19 .collect()
20}
21#[derive(Clone, PartialEq, Eq)]
22pub enum Projection {
23 None,
24 Move {
25 source: String,
26 target: String,
27 tasks: Vec<String>,
28 },
29 Ref {
30 workspace: String,
31 task: String,
32 reference: String,
33 descriptor: Vec<u8>,
34 deleted: bool,
35 version: Option<String>,
36 },
37 Unref {
38 workspace: String,
39 task: String,
40 reference: String,
41 },
42 Parent {
43 action: super::parent::ParentAction,
44 workspace: String,
45 task: String,
46 deleted: bool,
47 version: Option<String>,
48 },
49}
50impl Projection {
51 pub fn encode(&self) -> Vec<u8> {
52 match self {
53 Self::Move {
54 source,
55 target,
56 tasks,
57 } => {
58 let mut out = vec![4];
59 encode_text(&mut out, source);
60 encode_text(&mut out, target);
61 out.extend((tasks.len() as u16).to_be_bytes());
62 for task in tasks {
63 let mut bits = 0_u128;
64 for byte in task.bytes() {
65 bits = (bits << 5)
66 | BASE32
67 .iter()
68 .position(|v| *v == byte)
69 .expect("validated task ID") as u128;
70 }
71 out.extend_from_slice(&bits.to_be_bytes()[6..]);
72 }
73 return out;
74 }
75 Self::Ref {
76 workspace,
77 task,
78 reference,
79 descriptor,
80 deleted,
81 version,
82 } => {
83 let mut out = vec![2];
84 for text in [workspace, task, reference] {
85 encode_text(&mut out, text);
86 }
87 out.extend((descriptor.len() as u32).to_be_bytes());
88 out.extend(descriptor);
89 out.push(u8::from(*deleted));
90 out.push(u8::from(version.is_some()));
91 if let Some(v) = version {
92 encode_text(&mut out, v);
93 }
94 return out;
95 }
96 Self::Unref {
97 workspace,
98 task,
99 reference,
100 } => {
101 let mut out = vec![3];
102 for text in [workspace, task, reference] {
103 encode_text(&mut out, text);
104 }
105 return out;
106 }
107 _ => {}
108 }
109 let Self::Parent {
110 action,
111 workspace,
112 task,
113 deleted,
114 version,
115 } = self
116 else {
117 return vec![0];
118 };
119 let mut out = vec![1, *action as u8];
120 encode_text(&mut out, workspace);
121 encode_text(&mut out, task);
122 out.push(u8::from(*deleted));
123 out.push(u8::from(version.is_some()));
124 if let Some(v) = version {
125 encode_text(&mut out, v);
126 }
127 out
128 }
129 pub fn decode(input: &[u8]) -> Result<Self> {
130 decode_projection(input)
131 }
132}
133
134struct Reader<'a>(&'a [u8]);
135impl<'a> Reader<'a> {
136 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
137 valid(n <= self.0.len())?;
138 let (a, b) = self.0.split_at(n);
139 self.0 = b;
140 Ok(a)
141 }
142 fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
143 Ok(self.take(N)?.try_into()?)
144 }
145 fn blob(&mut self, max: usize) -> Result<&'a [u8]> {
146 let n = u32::from_be_bytes(self.array()?) as usize;
147 valid(n <= max)?;
148 self.take(n)
149 }
150}
151pub struct Envelope<'a> {
152 pub vault: [u8; 32],
153 pub stream: [u8; 32],
154 pub generation: [u8; 32],
155 pub id: String,
156 pub projection: Projection,
157 pub header: &'a [u8],
158 pub body: &'a [u8],
159 pub nonce: [u8; 24],
160}
161pub fn parse(record: &[u8]) -> Result<Envelope<'_>> {
162 valid(record.len() <= RECORD_LIMIT)?;
163 let mut r = Reader(record);
164 let header = r.blob(4544)?;
165 let body = r.blob(131072 + 16)?;
166 valid(r.0.is_empty() && body.len() >= 16)?;
167 let mut r = Reader(header);
168 valid(r.take(8)? == b"AVEN\x01\x00\x01\x01")?;
169 let vault = r.blob(32)?.try_into()?;
170 let stream = r.blob(32)?.try_into()?;
171 let generation = r.blob(32)?.try_into()?;
172 valid(r.blob(32)?.len() == 32)?;
173 let id = std::str::from_utf8(r.blob(256)?)?.to_owned();
174 valid(!id.is_empty())?;
175 valid(r.array::<4>()? == 18u32.to_be_bytes())?;
176 let projection = Projection::decode(r.blob(4096)?)?;
177 let nonce = r.blob(24)?.try_into()?;
178 valid(r.0.is_empty())?;
179 Ok(Envelope {
180 vault,
181 stream,
182 generation,
183 id,
184 projection,
185 header,
186 body,
187 nonce,
188 })
189}
190pub fn encode_text(out: &mut Vec<u8>, s: &str) {
191 bytes(out, s.as_bytes());
192}
193pub fn decode_projection(input: &[u8]) -> Result<Projection> {
194 let mut r = Reader(input);
195 let result = match r.take(1)?[0] {
196 0 => Projection::None,
197 1 => {
198 let action = super::parent::ParentAction::from_byte(r.take(1)?[0])
199 .ok_or_else(|| anyhow::anyhow!("error encrypted-tail-invalid"))?;
200 let workspace = std::str::from_utf8(r.blob(256)?)?.to_owned();
201 let task = std::str::from_utf8(r.blob(256)?)?.to_owned();
202 let deleted = r.take(1)?[0];
203 valid(deleted <= 1)?;
204 let version = match r.take(1)?[0] {
205 0 => None,
206 1 => Some(std::str::from_utf8(r.blob(256)?)?.to_owned()),
207 _ => anyhow::bail!("error encrypted-tail-projection"),
208 };
209 valid(
210 !workspace.is_empty()
211 && !task.is_empty()
212 && version.as_ref().is_none_or(|s| !s.is_empty()),
213 )?;
214 valid(
215 action != super::parent::ParentAction::Create
216 || (deleted == 0 && version.is_some()),
217 )?;
218 Projection::Parent {
219 action,
220 workspace,
221 task,
222 deleted: deleted == 1,
223 version,
224 }
225 }
226 kind @ (2 | 3) => {
227 let mut text = || -> Result<String> {
228 let s = std::str::from_utf8(r.blob(256)?)?.to_owned();
229 valid(!s.is_empty())?;
230 Ok(s)
231 };
232 let workspace = text()?;
233 let task = text()?;
234 let reference = text()?;
235 if kind == 3 {
236 Projection::Unref {
237 workspace,
238 task,
239 reference,
240 }
241 } else {
242 let descriptor = r.blob(1984)?.to_vec();
243 crate::images::Descriptor::decode(&descriptor)?;
244 let deleted = r.take(1)?[0];
245 valid(deleted <= 1)?;
246 let version = match r.take(1)?[0] {
247 0 => None,
248 1 => {
249 let s = std::str::from_utf8(r.blob(256)?)?.to_owned();
250 valid(!s.is_empty())?;
251 Some(s)
252 }
253 _ => anyhow::bail!("error encrypted-image-hint"),
254 };
255 Projection::Ref {
256 workspace,
257 task,
258 reference,
259 descriptor,
260 deleted: deleted == 1,
261 version,
262 }
263 }
264 }
265 4 => {
266 let source = std::str::from_utf8(r.blob(16)?)?.to_owned();
267 let target = std::str::from_utf8(r.blob(16)?)?.to_owned();
268 for id in [&source, &target] {
269 crate::workspace::validate(id)?;
270 }
271 valid(source != target)?;
272 let count = u16::from_be_bytes(r.array()?) as usize;
273 valid((1..=MAX_MOVE_TASKS).contains(&count))?;
274 let mut tasks: Vec<String> = Vec::with_capacity(count);
275 for _ in 0..count {
276 let mut bytes = [0_u8; 16];
277 bytes[6..].copy_from_slice(r.take(10)?);
278 let task = encode_task(&bytes[6..]);
279 valid(tasks.last().is_none_or(|previous| previous < &task))?;
280 tasks.push(task);
281 }
282 Projection::Move {
283 source,
284 target,
285 tasks,
286 }
287 }
288 _ => anyhow::bail!("error encrypted-tail-projection"),
289 };
290 valid(r.0.is_empty())?;
291 Ok(result)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn move_workspace_validation_keeps_fixed_bytes_and_refusals() {
300 let projection = Projection::Move {
301 source: "0123456789ABCDEF".into(),
302 target: "GHJKMNPQRSTVWXYZ".into(),
303 tasks: vec!["0000000000000000".into()],
304 };
305 let bytes = projection.encode();
306 assert_eq!(
307 hex::encode(&bytes),
308 "0400000010303132333435363738394142434445460000001047484a4b4d4e5051525354565758595a000100000000000000000000"
309 );
310 assert!(Projection::decode(&bytes).unwrap() == projection);
311 for field in [0, 1] {
312 for invalid in [
313 "0123456789ABCDE",
314 "0123456789abcdef",
315 "0123456789ABCDEI",
316 "0123456789ABCDEL",
317 "0123456789ABCDEO",
318 "0123456789ABCDEU",
319 "00000000000000é",
320 ] {
321 let mut bad = vec![4];
322 encode_text(
323 &mut bad,
324 if field == 0 {
325 invalid
326 } else {
327 "0123456789ABCDEF"
328 },
329 );
330 encode_text(
331 &mut bad,
332 if field == 1 {
333 invalid
334 } else {
335 "GHJKMNPQRSTVWXYZ"
336 },
337 );
338 bad.extend(1_u16.to_be_bytes());
339 bad.extend([0; 10]);
340 assert_eq!(
341 Projection::decode(&bad).err().unwrap().to_string(),
342 "workspace ID must be 16 Crockford Base32 characters"
343 );
344 }
345 }
346 }
347}