Skip to main content

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}