Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | //! Bundles: how objects travel between a workspace and a host. |
| 2 | //! |
| 3 | //! ```text |
| 4 | //! "VCSB1\n" |
| 5 | //! frame*: id[32] kind[1] codec[1] raw_len[u64 le] payload_len[u64 le] payload |
| 6 | //! end: 32 zero bytes |
| 7 | //! ``` |
| 8 | //! |
| 9 | //! The codec says how the payload is stored (0 raw, 1 zstd); hosts compress |
| 10 | //! and decompress. A receiver checks every object against its ID. Hosts keep |
| 11 | //! some objects as deltas (2: a base object's ID, then a delta on it, see |
| 12 | //! `delta`), but only in their own storage: bundles that travel never have them. |
| 13 | |
| 14 | use crate::object::{Kind, Object}; |
| 15 | use crate::{Id, Result, corrupt}; |
| 16 | |
| 17 | pub const MAGIC: &[u8] = b"VCSB1\n"; |
| 18 | pub const RAW: u8 = 0; |
| 19 | pub const ZSTD: u8 = 1; |
| 20 | /// Stored by a host only: the base's ID (32 bytes), then a git delta on it. |
| 21 | pub const DELTA: u8 = 2; |
| 22 | /// No single object may claim to be bigger than this. |
| 23 | pub const MAX_OBJECT: u64 = 256 << 20; |
| 24 | |
| 25 | /// One stored object as it travels: possibly compressed. |
| 26 | #[derive(Debug, Clone)] |
| 27 | pub struct Frame { |
| 28 | pub id: Id, |
| 29 | pub kind: Kind, |
| 30 | pub codec: u8, |
| 31 | pub raw_len: u64, |
| 32 | pub payload: Vec<u8>, |
| 33 | } |
| 34 | |
| 35 | impl Frame { |
| 36 | pub fn raw(object: &Object) -> Frame { |
| 37 | Frame { id: object.id(), kind: object.kind, codec: RAW, raw_len: object.data.len() as u64, payload: object.data |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | pub fn write_header(out: &mut Vec<u8>) { |
| 42 | out.extend_from_slice(MAGIC); |
| 43 | } |
| 44 | |
| 45 | pub fn write_frame(out: &mut Vec<u8>, f: &Frame) { |
| 46 | out.extend_from_slice(&f.id.0); |
| 47 | out.push(f.kind as u8); |
| 48 | out.push(f.codec); |
| 49 | out.extend_from_slice(&f.raw_len.to_le_bytes()); |
| 50 | out.extend_from_slice(&(f.payload.len() as u64).to_le_bytes()); |
| 51 | out.extend_from_slice(&f.payload); |
| 52 | } |
| 53 | |
| 54 | pub fn write_end(out: &mut Vec<u8>) { |
| 55 | out.extend_from_slice(&[0u8; 32]); |
| 56 | } |
| 57 | |
| 58 | /// Reads frames from a complete bundle. |
| 59 | pub fn read(bytes: &[u8]) -> Result<Vec<Frame>> { |
| 60 | read_frames(bytes, false) |
| 61 | } |
| 62 | |
| 63 | /// Reads frames a host stored itself, deltas included. |
| 64 | pub fn read_stored(bytes: &[u8]) -> Result<Vec<Frame>> { |
| 65 | read_frames(bytes, true) |
| 66 | } |
| 67 | |
| 68 | fn read_frames(bytes: &[u8], deltas: bool) -> Result<Vec<Frame>> { |
| 69 | if !bytes.starts_with(MAGIC) { |
| 70 | return Err(corrupt("not an Onset bundle")); |
| 71 | } |
| 72 | let mut at = MAGIC.len(); |
| 73 | let mut out = Vec::new(); |
| 74 | loop { |
| 75 | let id_bytes = bytes.get(at..at + 32).ok_or_else(|| corrupt("bundle ends early"))?; |
| 76 | if id_bytes.iter().all(|b| *b == 0) { |
| 77 | break; |
| 78 | } |
| 79 | let id = Id::from_slice(id_bytes).unwrap(); |
| 80 | let header = bytes.get(at + 32..at + 50).ok_or_else(|| corrupt("bundle ends early"))?; |
| 81 | let kind = Kind::from_u8(header[0]).ok_or_else(|| corrupt("bad object kind in bundle"))?; |
| 82 | let codec = header[1]; |
| 83 | if codec != RAW && codec != ZSTD && !(deltas && codec == DELTA) { |
| 84 | return Err(corrupt("unknown codec in bundle")); |
| 85 | } |
| 86 | let raw_len = u64::from_le_bytes(header[2..10].try_into().unwrap()); |
| 87 | let payload_len = u64::from_le_bytes(header[10..18].try_into().unwrap()); |
| 88 | if raw_len > MAX_OBJECT || payload_len > MAX_OBJECT { |
| 89 | return Err(crate::Error::Limit("an object in the bundle is too big".into())); |
| 90 | } |
| 91 | let start = at + 50; |
| 92 | let end = start + payload_len as usize; |
| 93 | let payload = bytes.get(start..end).ok_or_else(|| corrupt("bundle ends early"))?.to_vec(); |
| 94 | if codec == RAW && payload_len != raw_len { |
| 95 | return Err(corrupt("raw payload length mismatch")); |
| 96 | } |
| 97 | if codec == DELTA && payload_len < 32 { |
| 98 | return Err(corrupt("delta without its base")); |
| 99 | } |
| 100 | out.push(Frame { id, kind, codec, raw_len, payload }); |
| 101 | at = end; |
| 102 | } |
| 103 | Ok(out) |
| 104 | } |
| 105 | |
| 106 | /// Checks a decoded object against the ID its frame claimed. |
| 107 | pub fn verify(frame: &Frame, data: &[u8]) -> Result<()> { |
| 108 | if data.len() as u64 != frame.raw_len { |
| 109 | return Err(corrupt(format!("object {} has the wrong length", frame.id.short()))); |
| 110 | } |
| 111 | if Id::of(frame.kind as u8, data) != frame.id { |
| 112 | return Err(corrupt(format!("object {} doesn't match its ID", frame.id.short()))); |
| 113 | } |
| 114 | Ok(()) |
| 115 | } |
| 116 | |
| 117 | #[cfg(test)] |
| 118 | mod tests { |
| 119 | use super::*; |
| 120 | |
| 121 | #[test] |
| 122 | fn round_trip() { |
| 123 | let a = Object::blob(b"hello".to_vec()); |
| 124 | let b = Object::new(Kind::Tree, Vec::new()); |
| 125 | let mut out = Vec::new(); |
| 126 | write_header(&mut out); |
| 127 | write_frame(&mut out, &Frame::raw(&a)); |
| 128 | write_frame(&mut out, &Frame::raw(&b)); |
| 129 | write_end(&mut out); |
| 130 | let frames = read(&out).unwrap(); |
| 131 | assert_eq!(frames.len(), 2); |
| 132 | verify(&frames[0], &frames[0].payload).unwrap(); |
| 133 | assert!(verify(&frames[0], b"other").is_err()); |
| 134 | assert_eq!(frames[1].kind, Kind::Tree); |
| 135 | } |
| 136 | } |
| 137 |