Skip to main content

reddb_file/
graph_label_registry.rs

1//! Persisted graph label-registry frame.
2//!
3//! The storage engine owns label allocation, legacy seeds, and namespace
4//! semantics. This module owns only the durable byte contract.
5
6pub const GRAPH_LABEL_REGISTRY_MAX_LABEL_LEN: usize = 512;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct GraphLabelRegistryEntry {
10    pub id: u32,
11    pub namespace: u8,
12    pub label: String,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum GraphLabelRegistryFrameError {
17    LabelTooLong { len: usize, max: usize },
18    Malformed { offset: usize, reason: &'static str },
19}
20
21impl std::fmt::Display for GraphLabelRegistryFrameError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Self::LabelTooLong { len, max } => {
25                write!(f, "graph label too long: {len} bytes (max {max})")
26            }
27            Self::Malformed { offset, reason } => {
28                write!(
29                    f,
30                    "malformed graph label registry at offset {offset}: {reason}"
31                )
32            }
33        }
34    }
35}
36
37impl std::error::Error for GraphLabelRegistryFrameError {}
38
39pub fn encode_graph_label_registry_frame(
40    entries: &[GraphLabelRegistryEntry],
41) -> Result<Vec<u8>, GraphLabelRegistryFrameError> {
42    let mut buf = Vec::with_capacity(4 + entries.len() * 16);
43    buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
44    for entry in entries {
45        let bytes = entry.label.as_bytes();
46        if bytes.len() > GRAPH_LABEL_REGISTRY_MAX_LABEL_LEN {
47            return Err(GraphLabelRegistryFrameError::LabelTooLong {
48                len: bytes.len(),
49                max: GRAPH_LABEL_REGISTRY_MAX_LABEL_LEN,
50            });
51        }
52        let len =
53            u16::try_from(bytes.len()).map_err(|_| GraphLabelRegistryFrameError::LabelTooLong {
54                len: bytes.len(),
55                max: GRAPH_LABEL_REGISTRY_MAX_LABEL_LEN,
56            })?;
57
58        buf.extend_from_slice(&entry.id.to_le_bytes());
59        buf.push(entry.namespace);
60        buf.extend_from_slice(&len.to_le_bytes());
61        buf.extend_from_slice(bytes);
62    }
63    Ok(buf)
64}
65
66pub fn decode_graph_label_registry_frame(
67    data: &[u8],
68) -> Result<Vec<GraphLabelRegistryEntry>, GraphLabelRegistryFrameError> {
69    if data.len() < 4 {
70        return Err(GraphLabelRegistryFrameError::Malformed {
71            offset: 0,
72            reason: "header truncated",
73        });
74    }
75
76    let count = u32::from_le_bytes(data[0..4].try_into().expect("header length checked")) as usize;
77    let mut off = 4;
78    let mut entries = Vec::with_capacity(count);
79    for _ in 0..count {
80        if data.len() < off + 7 {
81            return Err(GraphLabelRegistryFrameError::Malformed {
82                offset: off,
83                reason: "entry header truncated",
84            });
85        }
86        let id = u32::from_le_bytes(data[off..off + 4].try_into().expect("entry length checked"));
87        let namespace = data[off + 4];
88        let len = u16::from_le_bytes(
89            data[off + 5..off + 7]
90                .try_into()
91                .expect("entry length checked"),
92        ) as usize;
93        off += 7;
94        if data.len() < off + len {
95            return Err(GraphLabelRegistryFrameError::Malformed {
96                offset: off,
97                reason: "label bytes truncated",
98            });
99        }
100        let label = std::str::from_utf8(&data[off..off + len])
101            .map_err(|_| GraphLabelRegistryFrameError::Malformed {
102                offset: off,
103                reason: "label not utf8",
104            })?
105            .to_string();
106        entries.push(GraphLabelRegistryEntry {
107            id,
108            namespace,
109            label,
110        });
111        off += len;
112    }
113    Ok(entries)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn registry_frame_round_trips() {
122        let entries = vec![
123            GraphLabelRegistryEntry {
124                id: 1,
125                namespace: 0,
126                label: "host".to_string(),
127            },
128            GraphLabelRegistryEntry {
129                id: 64,
130                namespace: 1,
131                label: "purchased".to_string(),
132            },
133        ];
134        let encoded = encode_graph_label_registry_frame(&entries).unwrap();
135        let decoded = decode_graph_label_registry_frame(&encoded).unwrap();
136        assert_eq!(decoded, entries);
137    }
138
139    #[test]
140    fn registry_frame_rejects_truncated_input() {
141        assert!(matches!(
142            decode_graph_label_registry_frame(&[1, 2, 3]),
143            Err(GraphLabelRegistryFrameError::Malformed { .. })
144        ));
145        let encoded = encode_graph_label_registry_frame(&[GraphLabelRegistryEntry {
146            id: 64,
147            namespace: 0,
148            label: "abcd".to_string(),
149        }])
150        .unwrap();
151        assert!(matches!(
152            decode_graph_label_registry_frame(&encoded[..encoded.len() - 1]),
153            Err(GraphLabelRegistryFrameError::Malformed { .. })
154        ));
155    }
156}