Skip to main content

phi_ext/pxb/
fields.rs

1//! Tagged-field payloads (protobuf-style, fixed-width).
2//!
3//! Layout per field: `tag u16 | kind u8 | value…`. Only two wire kinds exist,
4//! so unknown fields are always skippable without a schema. Decoders must skip
5//! unknown tags; omitting a tag means the zero value.
6//!
7//! Evolution rules (see `doc/extensions.md`):
8//! - new fields take a new tag number, never reuse a tag;
9//! - empty strings/blobs are omitted on encode;
10//! - experimental tags use 128+ and must remain skippable.
11
12use super::codec::{Error, Error::*};
13
14/// 8-byte little-endian (u16/u32/bool/event codes share this kind).
15pub const WIRE_U64: u8 = 1;
16/// u32 length + bytes.
17pub const WIRE_BYTES: u8 = 2;
18
19/// Builds a tagged-field payload.
20pub struct FieldWriter {
21    buf: Vec<u8>,
22}
23
24impl Default for FieldWriter {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl FieldWriter {
31    pub fn new() -> Self {
32        Self {
33            buf: Vec::with_capacity(32),
34        }
35    }
36
37    /// Returns the encoded payload.
38    pub fn bytes(&self) -> &[u8] {
39        &self.buf
40    }
41
42    pub fn into_vec(self) -> Vec<u8> {
43        self.buf
44    }
45
46    fn put_hdr(&mut self, tag: u16, kind: u8) {
47        self.buf.extend_from_slice(&tag.to_le_bytes());
48        self.buf.push(kind);
49    }
50
51    /// Writes an 8-byte integer field. Always written, even when zero —
52    /// decoders rely on the tag being present for bool-ish fields.
53    pub fn put_u64(&mut self, tag: u16, v: u64) {
54        self.put_hdr(tag, WIRE_U64);
55        self.buf.extend_from_slice(&v.to_le_bytes());
56    }
57
58    pub fn put_u16(&mut self, tag: u16, v: u16) {
59        self.put_u64(tag, u64::from(v));
60    }
61
62    pub fn put_u32(&mut self, tag: u16, v: u32) {
63        self.put_u64(tag, u64::from(v));
64    }
65
66    pub fn put_bool(&mut self, tag: u16, v: bool) {
67        self.put_u64(tag, u64::from(v));
68    }
69
70    /// Writes a length-prefixed blob. Empty blobs are omitted so decoders stay
71    /// compact on the hot path.
72    pub fn put_bytes(&mut self, tag: u16, p: &[u8]) {
73        if p.is_empty() {
74            return;
75        }
76        self.put_hdr(tag, WIRE_BYTES);
77        self.buf.extend_from_slice(&(p.len() as u32).to_le_bytes());
78        self.buf.extend_from_slice(p);
79    }
80
81    pub fn put_string(&mut self, tag: u16, s: &str) {
82        if s.is_empty() {
83            return;
84        }
85        self.put_bytes(tag, s.as_bytes());
86    }
87
88    /// Writes a packed list as `WireBytes`: `u16 count` + values.
89    pub fn put_u16s(&mut self, tag: u16, vs: &[u16]) {
90        if vs.is_empty() {
91            return;
92        }
93        let mut inner = Vec::with_capacity(2 + vs.len() * 2);
94        inner.extend_from_slice(&(vs.len() as u16).to_le_bytes());
95        for v in vs {
96            inner.extend_from_slice(&v.to_le_bytes());
97        }
98        self.put_bytes(tag, &inner);
99    }
100}
101
102/// Walks a tagged-field payload.
103pub struct FieldReader<'a> {
104    b: &'a [u8],
105    i: usize,
106}
107
108impl<'a> FieldReader<'a> {
109    pub fn new(b: &'a [u8]) -> Self {
110        Self { b, i: 0 }
111    }
112
113    pub fn done(&self) -> bool {
114        self.i >= self.b.len()
115    }
116
117    fn need(&self, n: usize) -> Result<(), Error> {
118        if self.b.len() - self.i < n {
119            return Err(Truncated);
120        }
121        Ok(())
122    }
123
124    /// Returns the next field tag and wire kind.
125    pub fn next_field(&mut self) -> Result<(u16, u8), Error> {
126        if self.done() {
127            return Err(Truncated);
128        }
129        self.need(3)?;
130        let tag = u16::from_le_bytes([self.b[self.i], self.b[self.i + 1]]);
131        let kind = self.b[self.i + 2];
132        self.i += 3;
133        if kind != WIRE_U64 && kind != WIRE_BYTES {
134            return Err(BadWire);
135        }
136        if tag == 0 {
137            return Err(BadTag);
138        }
139        Ok((tag, kind))
140    }
141
142    /// Reads a `WireU64` value (call after `next` returned `WireU64`).
143    pub fn u64(&mut self) -> Result<u64, Error> {
144        self.need(8)?;
145        let v = u64::from_le_bytes(self.b[self.i..self.i + 8].try_into().unwrap());
146        self.i += 8;
147        Ok(v)
148    }
149
150    /// Reads a `WireBytes` value (call after `next` returned `WireBytes`).
151    /// The returned slice aliases the payload.
152    pub fn bytes(&mut self) -> Result<&'a [u8], Error> {
153        self.need(4)?;
154        let n = u32::from_le_bytes(self.b[self.i..self.i + 4].try_into().unwrap()) as usize;
155        self.i += 4;
156        self.need(n)?;
157        let out = &self.b[self.i..self.i + n];
158        self.i += n;
159        Ok(out)
160    }
161
162    /// Discards the value for `kind`.
163    pub fn skip(&mut self, kind: u8) -> Result<(), Error> {
164        match kind {
165            WIRE_U64 => {
166                self.u64()?;
167                Ok(())
168            }
169            WIRE_BYTES => {
170                self.bytes()?;
171                Ok(())
172            }
173            _ => Err(BadWire),
174        }
175    }
176}
177
178/// Calls `f` for each field; unknown tags should be skipped via the reader.
179/// `f` may call `u64`/`bytes` exactly once for the current field, or `skip`.
180pub fn walk_fields(
181    b: &[u8],
182    mut f: impl FnMut(u16, u8, &mut FieldReader<'_>) -> Result<(), Error>,
183) -> Result<(), Error> {
184    let mut fr = FieldReader::new(b);
185    while !fr.done() {
186        let (tag, kind) = fr.next_field()?;
187        let before = fr.i;
188        f(tag, kind, &mut fr)?;
189        // If the callback neither consumed nor skipped, skip for them.
190        if fr.i == before {
191            fr.skip(kind)?;
192        }
193    }
194    Ok(())
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn writer_reader_roundtrip() {
203        let mut w = FieldWriter::new();
204        w.put_u64(1, 42);
205        w.put_u16(2, 7);
206        w.put_bool(3, true);
207        w.put_bool(4, false);
208        w.put_string(5, "hello");
209        w.put_bytes(6, &[1, 2, 3]);
210        w.put_u16s(7, &[5, 10]);
211
212        let mut seen = Vec::new();
213        walk_fields(w.bytes(), |tag, kind, fr| {
214            let v = match kind {
215                WIRE_U64 => fr.u64().unwrap(),
216                WIRE_BYTES => {
217                    let b = fr.bytes().unwrap();
218                    b.len() as u64
219                }
220                _ => unreachable!(),
221            };
222            seen.push((tag, v));
223            Ok(())
224        })
225        .unwrap();
226        assert_eq!(
227            seen,
228            vec![(1, 42), (2, 7), (3, 1), (4, 0), (5, 5), (6, 3), (7, 6)]
229        );
230    }
231
232    #[test]
233    fn empty_values_are_omitted() {
234        let mut w = FieldWriter::new();
235        w.put_string(1, "");
236        w.put_bytes(2, &[]);
237        w.put_u16s(3, &[]);
238        assert!(w.bytes().is_empty());
239    }
240
241    #[test]
242    fn unknown_tags_are_skippable() {
243        let mut w = FieldWriter::new();
244        w.put_u64(1, 9);
245        w.put_string(128, "experimental");
246        w.put_string(2, "known");
247
248        let mut name = String::new();
249        walk_fields(w.bytes(), |tag, kind, fr| {
250            match tag {
251                2 => name = String::from_utf8_lossy(fr.bytes().unwrap()).into_owned(),
252                _ => fr.skip(kind)?,
253            }
254            Ok(())
255        })
256        .unwrap();
257        assert_eq!(name, "known");
258    }
259
260    #[test]
261    fn rejects_bad_wire_kinds() {
262        let mut w = FieldWriter::new();
263        w.buf = vec![1, 0, 7]; // tag 1, unknown kind 7, no value
264        let err = walk_fields(w.bytes(), |_t, _k, _fr| Ok(())).unwrap_err();
265        assert!(matches!(err, Error::BadWire));
266    }
267
268    #[test]
269    fn truncated_payload_errors() {
270        let mut w = FieldWriter::new();
271        w.put_u64(1, 42);
272        let bytes = w.into_vec();
273        let err = walk_fields(&bytes[..3], |_t, _k, _fr| Ok(())).unwrap_err();
274        assert!(matches!(err, Error::Truncated));
275    }
276}