iris_abi/record.rs
1//! Record framing.
2//!
3//! Every message that crosses the boundary is a tagged, versioned, length-prefixed record. The
4//! length is what makes the format extensible: a reader that does not recognise a tag steps over
5//! the record instead of giving up, and a reader that recognises a tag but was compiled before some
6//! of its fields existed reads the fields it knows and steps over the rest.
7//!
8//! That is the whole compatibility story, and it is deliberately small enough to hold in your head.
9//! The rules are written out in `docs/ABI.md` and the tests in `tests/forward_compat.rs` are there
10//! to stop anybody quietly breaking them.
11
12use core::fmt;
13
14use crate::error::{Error, Result};
15use crate::wire::{Reader, Writer, align_up};
16
17/// Which kind of record this is.
18///
19/// This is a newtype over `u16` rather than an enum on purpose. An enum would make an unknown tag
20/// unrepresentable, and being able to represent an unknown tag is exactly what a reader needs in
21/// order to skip one.
22#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
23pub struct Tag(pub u16);
24
25impl Tag {
26 /// The host introducing itself and saying what it can do.
27 pub const HELLO: Self = Self(0x0001);
28 /// The decoder answering, and saying what it needs.
29 pub const HELLO_ACK: Self = Self(0x0002);
30 /// Either side declining to go on, with a reason.
31 pub const REFUSAL: Self = Self(0x0003);
32 /// The host asking the decoder for a run of rows.
33 pub const SCAN_REQUEST: Self = Self(0x0010);
34 /// The decoder asking for bytes of the source it has not been given yet.
35 pub const RANGE_REQUEST: Self = Self(0x0020);
36 /// The decoder handing back one batch of decoded rows.
37 pub const BATCH: Self = Self(0x0030);
38
39 /// The first tag in the range reserved for private extensions.
40 ///
41 /// Nothing in this range will ever be assigned a meaning by us, so anybody can use it for their
42 /// own records without having to worry about a future version of iris colliding with them.
43 pub const EXPERIMENTAL_BASE: Self = Self(0xFF00);
44
45 /// Whether this tag is in the private extension range.
46 #[must_use]
47 pub const fn is_experimental(self) -> bool {
48 self.0 >= Self::EXPERIMENTAL_BASE.0
49 }
50
51 /// The name of this tag, if it is one we assigned.
52 #[must_use]
53 pub const fn name(self) -> Option<&'static str> {
54 match self {
55 Self::HELLO => Some("Hello"),
56 Self::HELLO_ACK => Some("HelloAck"),
57 Self::REFUSAL => Some("Refusal"),
58 Self::SCAN_REQUEST => Some("ScanRequest"),
59 Self::RANGE_REQUEST => Some("RangeRequest"),
60 Self::BATCH => Some("Batch"),
61 _ => None,
62 }
63 }
64}
65
66impl fmt::Display for Tag {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self.name() {
69 Some(name) => write!(f, "{name} (0x{:04x})", self.0),
70 None if self.is_experimental() => write!(f, "experimental 0x{:04x}", self.0),
71 None => write!(f, "unknown 0x{:04x}", self.0),
72 }
73 }
74}
75
76/// The eight bytes in front of every record payload.
77#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78pub struct Header {
79 /// Which record this is.
80 pub tag: Tag,
81 /// Which version of that record's layout the payload uses.
82 ///
83 /// A version only goes up when a field is removed or changes meaning. Adding a field at the end
84 /// does not need a new version, because a reader that does not know about the field skips it.
85 pub version: u16,
86 /// How many bytes of payload follow, not counting the padding after them.
87 pub len: u32,
88}
89
90impl Header {
91 /// How wide a header is, in bytes.
92 pub const SIZE: usize = 8;
93}
94
95impl<'a> Reader<'a> {
96 /// Reads a record header and returns a reader over just that record's payload.
97 ///
98 /// The outer reader is left pointing at the next record, past the payload and its padding, so a
99 /// caller that does not care about this record can simply drop the payload reader.
100 ///
101 /// # Errors
102 ///
103 /// Returns [`Error::Truncated`] if the header or the payload runs off the end, or
104 /// [`Error::LengthOverflow`] if the declared length does not fit in a `usize`.
105 pub fn record(&mut self) -> Result<(Header, Reader<'a>)> {
106 let tag = Tag(self.u16()?);
107 let version = self.u16()?;
108 let len = self.u32()?;
109 let payload_len = usize::try_from(len).map_err(|_| Error::LengthOverflow)?;
110 let padded = align_up(payload_len);
111 let mut body = self.sub(padded.min(self.remaining()))?;
112 if body.remaining() < payload_len {
113 return Err(Error::Truncated {
114 needed: payload_len,
115 available: body.remaining(),
116 });
117 }
118 let payload = body.sub(payload_len)?;
119 Ok((Header { tag, version, len }, payload))
120 }
121}
122
123impl Writer<'_> {
124 /// Writes a record, filling in its length once the body has been written.
125 ///
126 /// # Errors
127 ///
128 /// Returns [`Error::BufferFull`] if the buffer runs out, [`Error::LengthOverflow`] if the body
129 /// is longer than a `u32` can describe, or whatever the body itself returns.
130 pub fn record(
131 &mut self,
132 tag: Tag,
133 version: u16,
134 body: impl FnOnce(&mut Self) -> Result<()>,
135 ) -> Result<()> {
136 self.u16(tag.0)?;
137 self.u16(version)?;
138 let len_at = self.position();
139 self.u32(0)?;
140 let start = self.position();
141 body(self)?;
142 let len = u32::try_from(self.position() - start).map_err(|_| Error::LengthOverflow)?;
143 self.patch_u32(len_at, len)?;
144 self.align()
145 }
146}