Skip to main content

common/serde/
key_prefix.rs

1//! Common record key prefix encoding for OpenData storage systems.
2//!
3//! This module implements RFC 0001: Record Key Prefix. All OpenData records
4//! stored in SlateDB use keys with a standardized 2-byte prefix:
5//!
6//! ```text
7//! ┌─────────┬────────────┬─────────────────────┐
8//! │ version │ record_tag │  ... record fields  │
9//! │ 1 byte  │   1 byte   │    (varies)         │
10//! └─────────┴────────────┴─────────────────────┘
11//! ```
12//!
13//! # Version Byte
14//!
15//! The first byte identifies the key format version. Each subsystem manages
16//! its version independently.
17//!
18//! # Record Tag Byte
19//!
20//! The second byte is a composite tag:
21//!
22//! ```text
23//! ┌────────────┬────────────┐
24//! │  bits 7-4  │  bits 3-0  │
25//! │ record type│  reserved  │
26//! └────────────┴────────────┘
27//! ```
28//!
29//! - **Record Type (high 4 bits):** Identifies the kind of record (values 0x1–0xF).
30//! - **Reserved (low 4 bits):** Subsystem-specific use (e.g., bucket granularity).
31
32use bytes::{BufMut, Bytes, BytesMut};
33
34use super::DeserializeError;
35
36/// A 2-byte key prefix containing version and record tag.
37///
38/// This type encapsulates the standard prefix used by all OpenData records.
39/// It provides methods for serialization, deserialization, and validation.
40///
41/// # Examples
42///
43/// ```
44/// use common::serde::key_prefix::{KeyPrefix, RecordTag};
45///
46/// // Create a prefix
47/// let prefix = KeyPrefix::new(0x01, RecordTag::new(0x02, 0x00));
48/// assert_eq!(prefix.version(), 0x01);
49/// assert_eq!(prefix.tag().record_type(), 0x02);
50///
51/// // Serialize and deserialize
52/// let bytes = prefix.to_bytes();
53/// let parsed = KeyPrefix::from_bytes(&bytes).unwrap();
54/// assert_eq!(parsed, prefix);
55/// ```
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct KeyPrefix {
58    version: u8,
59    tag: RecordTag,
60}
61
62impl KeyPrefix {
63    /// Creates a new key prefix with the given version and record tag.
64    pub fn new(version: u8, tag: RecordTag) -> Self {
65        Self { version, tag }
66    }
67
68    /// Returns the version byte.
69    pub fn version(&self) -> u8 {
70        self.version
71    }
72
73    /// Returns the record tag.
74    pub fn tag(&self) -> RecordTag {
75        self.tag
76    }
77
78    /// Parses a key prefix from a byte slice.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if:
83    /// - The buffer is too short (less than 2 bytes)
84    /// - The record type is 0 (reserved)
85    pub fn from_bytes(data: &[u8]) -> Result<Self, DeserializeError> {
86        if data.len() < 2 {
87            return Err(DeserializeError {
88                message: format!(
89                    "buffer too short for key prefix: need 2 bytes, got {}",
90                    data.len()
91                ),
92            });
93        }
94        let version = data[0];
95        let tag = RecordTag::from_byte(data[1])?;
96        Ok(Self { version, tag })
97    }
98
99    /// Parses a key prefix, validating the version matches expected.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if:
104    /// - The buffer is too short
105    /// - The version doesn't match
106    /// - The record type is 0
107    pub fn from_bytes_versioned(
108        data: &[u8],
109        expected_version: u8,
110    ) -> Result<Self, DeserializeError> {
111        let prefix = Self::from_bytes(data)?;
112        if prefix.version != expected_version {
113            return Err(DeserializeError {
114                message: format!(
115                    "invalid key version: expected 0x{:02x}, got 0x{:02x}",
116                    expected_version, prefix.version
117                ),
118            });
119        }
120        Ok(prefix)
121    }
122
123    /// Serializes the prefix to a 2-byte array.
124    pub fn to_bytes(&self) -> Bytes {
125        Bytes::from(vec![self.version, self.tag.as_byte()])
126    }
127
128    /// Writes the prefix to a buffer.
129    pub fn write_to(&self, buf: &mut BytesMut) {
130        buf.put_u8(self.version);
131        buf.put_u8(self.tag.as_byte());
132    }
133}
134
135/// Record tag combining record type (high 4 bits) and reserved bits (low 4 bits).
136///
137/// The record tag is the second byte of the key prefix. It encodes the record
138/// type in the high 4 bits, leaving the low 4 bits for subsystem-specific use.
139///
140/// # Examples
141///
142/// ```
143/// use common::serde::key_prefix::RecordTag;
144///
145/// // Create a tag with type 0x01 and reserved bits 0x00
146/// let tag = RecordTag::new(0x01, 0x00);
147/// assert_eq!(tag.record_type(), 0x01);
148/// assert_eq!(tag.reserved(), 0x00);
149///
150/// // Create a tag with type 0x05 and reserved bits 0x03
151/// let tag = RecordTag::new(0x05, 0x03);
152/// assert_eq!(tag.as_byte(), 0x53);
153/// ```
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct RecordTag(u8);
156
157impl RecordTag {
158    /// Creates a new record tag with the given record type and reserved bits.
159    ///
160    /// # Panics
161    ///
162    /// Panics if `record_type` is 0 or greater than 15, or if `reserved` is
163    /// greater than 15.
164    pub fn new(record_type: u8, reserved: u8) -> Self {
165        assert!(
166            record_type > 0 && record_type <= 0x0F,
167            "record type {} must be in range 1-15",
168            record_type
169        );
170        assert!(
171            reserved <= 0x0F,
172            "reserved bits {} must be in range 0-15",
173            reserved
174        );
175        RecordTag((record_type << 4) | reserved)
176    }
177
178    /// Creates a record tag from a raw byte value.
179    ///
180    /// Returns an error if the record type (high 4 bits) is 0.
181    pub fn from_byte(byte: u8) -> Result<Self, DeserializeError> {
182        let record_type = (byte & 0xF0) >> 4;
183        if record_type == 0 {
184            return Err(DeserializeError {
185                message: format!(
186                    "invalid record tag: 0x{:02x} (record type 0 is reserved)",
187                    byte
188                ),
189            });
190        }
191        Ok(RecordTag(byte))
192    }
193
194    /// Returns the record type (high 4 bits).
195    pub fn record_type(&self) -> u8 {
196        (self.0 & 0xF0) >> 4
197    }
198
199    /// Returns the reserved bits (low 4 bits).
200    pub fn reserved(&self) -> u8 {
201        self.0 & 0x0F
202    }
203
204    /// Returns the raw byte representation.
205    pub fn as_byte(&self) -> u8 {
206        self.0
207    }
208
209    /// Returns a new record tag with the same record type but different reserved bits.
210    ///
211    /// # Panics
212    ///
213    /// Panics if `reserved` is greater than 15.
214    pub fn with_reserved(&self, reserved: u8) -> Self {
215        assert!(
216            reserved <= 0x0F,
217            "reserved bits {} must be in range 0-15",
218            reserved
219        );
220        RecordTag((self.0 & 0xF0) | reserved)
221    }
222
223    /// Returns a range covering all tags with the given record type.
224    ///
225    /// This is useful for creating scan ranges that match all records of a
226    /// given type regardless of their reserved bits.
227    pub fn type_range(record_type: u8) -> std::ops::Range<u8> {
228        assert!(
229            record_type > 0 && record_type <= 0x0F,
230            "record type {} must be in range 1-15",
231            record_type
232        );
233        let start = record_type << 4;
234        start..(start + 0x10)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn should_create_record_tag() {
244        // given
245        let record_type = 0x05;
246        let reserved = 0x03;
247
248        // when
249        let tag = RecordTag::new(record_type, reserved);
250
251        // then
252        assert_eq!(tag.as_byte(), 0x53);
253        assert_eq!(tag.record_type(), 0x05);
254        assert_eq!(tag.reserved(), 0x03);
255    }
256
257    #[test]
258    fn should_create_tag_with_zero_reserved() {
259        // given
260        let record_type = 0x01;
261        let reserved = 0x00;
262
263        // when
264        let tag = RecordTag::new(record_type, reserved);
265
266        // then
267        assert_eq!(tag.as_byte(), 0x10);
268        assert_eq!(tag.record_type(), 0x01);
269        assert_eq!(tag.reserved(), 0x00);
270    }
271
272    #[test]
273    fn should_create_tag_with_max_values() {
274        // given
275        let record_type = 0x0F;
276        let reserved = 0x0F;
277
278        // when
279        let tag = RecordTag::new(record_type, reserved);
280
281        // then
282        assert_eq!(tag.as_byte(), 0xFF);
283        assert_eq!(tag.record_type(), 0x0F);
284        assert_eq!(tag.reserved(), 0x0F);
285    }
286
287    #[test]
288    #[should_panic(expected = "record type 0 must be in range 1-15")]
289    fn should_panic_on_zero_record_type() {
290        RecordTag::new(0, 0);
291    }
292
293    #[test]
294    #[should_panic(expected = "record type 16 must be in range 1-15")]
295    fn should_panic_on_record_type_overflow() {
296        RecordTag::new(16, 0);
297    }
298
299    #[test]
300    #[should_panic(expected = "reserved bits 16 must be in range 0-15")]
301    fn should_panic_on_reserved_overflow() {
302        RecordTag::new(1, 16);
303    }
304
305    #[test]
306    fn should_parse_tag_from_byte() {
307        // given
308        let byte = 0x53;
309
310        // when
311        let tag = RecordTag::from_byte(byte).unwrap();
312
313        // then
314        assert_eq!(tag.record_type(), 0x05);
315        assert_eq!(tag.reserved(), 0x03);
316    }
317
318    #[test]
319    fn should_reject_zero_record_type_byte() {
320        // given
321        let byte = 0x0F; // record type 0, reserved 15
322
323        // when
324        let result = RecordTag::from_byte(byte);
325
326        // then
327        assert!(result.is_err());
328        assert!(
329            result
330                .unwrap_err()
331                .message
332                .contains("record type 0 is reserved")
333        );
334    }
335
336    #[test]
337    fn should_compute_type_range() {
338        // given
339        let record_type = 0x03;
340
341        // when
342        let range = RecordTag::type_range(record_type);
343
344        // then
345        assert_eq!(range.start, 0x30);
346        assert_eq!(range.end, 0x40);
347    }
348
349    #[test]
350    fn should_create_tag_with_different_reserved_bits() {
351        // given
352        let tag = RecordTag::new(0x05, 0x00);
353
354        // when
355        let new_tag = tag.with_reserved(0x0A);
356
357        // then
358        assert_eq!(new_tag.record_type(), 0x05);
359        assert_eq!(new_tag.reserved(), 0x0A);
360        assert_eq!(new_tag.as_byte(), 0x5A);
361    }
362
363    #[test]
364    #[should_panic(expected = "reserved bits 16 must be in range 0-15")]
365    fn should_panic_on_with_reserved_overflow() {
366        let tag = RecordTag::new(0x01, 0x00);
367        tag.with_reserved(16);
368    }
369
370    #[test]
371    fn should_create_key_prefix() {
372        // given
373        let version = 0x01;
374        let tag = RecordTag::new(0x02, 0x05);
375
376        // when
377        let prefix = KeyPrefix::new(version, tag);
378
379        // then
380        assert_eq!(prefix.version(), version);
381        assert_eq!(prefix.tag().as_byte(), tag.as_byte());
382    }
383
384    #[test]
385    fn should_write_and_read_key_prefix() {
386        // given
387        let prefix = KeyPrefix::new(0x01, RecordTag::new(0x02, 0x05));
388        let mut buf = BytesMut::new();
389
390        // when
391        prefix.write_to(&mut buf);
392        let parsed = KeyPrefix::from_bytes(&buf).unwrap();
393
394        // then
395        assert_eq!(parsed, prefix);
396    }
397
398    #[test]
399    fn should_serialize_key_prefix_to_bytes() {
400        // given
401        let prefix = KeyPrefix::new(0x01, RecordTag::new(0x02, 0x05));
402
403        // when
404        let bytes = prefix.to_bytes();
405
406        // then
407        assert_eq!(bytes.len(), 2);
408        assert_eq!(bytes[0], 0x01);
409        assert_eq!(bytes[1], 0x25);
410    }
411
412    #[test]
413    fn should_parse_key_prefix_versioned() {
414        // given
415        let expected_version = 0x01;
416        let data = [expected_version, 0x25]; // version 1, type 2, reserved 5
417
418        // when
419        let prefix = KeyPrefix::from_bytes_versioned(&data, expected_version).unwrap();
420
421        // then
422        assert_eq!(prefix.version(), expected_version);
423        assert_eq!(prefix.tag().record_type(), 0x02);
424        assert_eq!(prefix.tag().reserved(), 0x05);
425    }
426
427    #[test]
428    fn should_reject_wrong_version() {
429        // given
430        let data = [0x02, 0x10]; // wrong version
431
432        // when
433        let result = KeyPrefix::from_bytes_versioned(&data, 0x01);
434
435        // then
436        assert!(result.is_err());
437        assert!(result.unwrap_err().message.contains("invalid key version"));
438    }
439
440    #[test]
441    fn should_reject_short_buffer() {
442        // given
443        let data = [0x01]; // only 1 byte
444
445        // when
446        let result = KeyPrefix::from_bytes(&data);
447
448        // then
449        assert!(result.is_err());
450        assert!(result.unwrap_err().message.contains("buffer too short"));
451    }
452}