reddb_file/
graph_table_index.rs1use crate::{decode_graph_table_ref, encode_graph_table_ref, GraphTableRef, GRAPH_TABLE_REF_SIZE};
7
8pub const GRAPH_TABLE_INDEX_ENTRY_HEADER_LEN: usize = 2;
9pub const GRAPH_TABLE_INDEX_HEADER_LEN: usize = 4;
10pub const GRAPH_TABLE_INDEX_MAX_NODE_ID_LEN: usize = u16::MAX as usize;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct GraphTableIndexEntry {
14 pub node_id: String,
15 pub table_ref: GraphTableRef,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum GraphTableIndexFrameError {
20 TooManyEntries { len: usize, max: usize },
21 NodeIdTooLong { len: usize, max: usize },
22 Malformed { offset: usize, reason: &'static str },
23}
24
25impl std::fmt::Display for GraphTableIndexFrameError {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Self::TooManyEntries { len, max } => {
29 write!(f, "too many graph-table index entries: {len} (max {max})")
30 }
31 Self::NodeIdTooLong { len, max } => {
32 write!(f, "graph-table node id too long: {len} bytes (max {max})")
33 }
34 Self::Malformed { offset, reason } => {
35 write!(
36 f,
37 "malformed graph-table index at offset {offset}: {reason}"
38 )
39 }
40 }
41 }
42}
43
44impl std::error::Error for GraphTableIndexFrameError {}
45
46pub fn encode_graph_table_index_frame(
47 entries: &[GraphTableIndexEntry],
48) -> Result<Vec<u8>, GraphTableIndexFrameError> {
49 let entry_count =
50 u32::try_from(entries.len()).map_err(|_| GraphTableIndexFrameError::TooManyEntries {
51 len: entries.len(),
52 max: u32::MAX as usize,
53 })?;
54
55 let mut buf = Vec::with_capacity(
56 GRAPH_TABLE_INDEX_HEADER_LEN
57 + entries.len() * (GRAPH_TABLE_INDEX_ENTRY_HEADER_LEN + GRAPH_TABLE_REF_SIZE),
58 );
59 buf.extend_from_slice(&entry_count.to_le_bytes());
60
61 for entry in entries {
62 let id_bytes = entry.node_id.as_bytes();
63 let id_len = u16::try_from(id_bytes.len()).map_err(|_| {
64 GraphTableIndexFrameError::NodeIdTooLong {
65 len: id_bytes.len(),
66 max: GRAPH_TABLE_INDEX_MAX_NODE_ID_LEN,
67 }
68 })?;
69
70 buf.extend_from_slice(&id_len.to_le_bytes());
71 buf.extend_from_slice(id_bytes);
72 buf.extend_from_slice(&encode_graph_table_ref(entry.table_ref));
73 }
74
75 Ok(buf)
76}
77
78pub fn decode_graph_table_index_frame(
79 data: &[u8],
80) -> Result<Vec<GraphTableIndexEntry>, GraphTableIndexFrameError> {
81 if data.len() < GRAPH_TABLE_INDEX_HEADER_LEN {
82 return Err(GraphTableIndexFrameError::Malformed {
83 offset: 0,
84 reason: "header truncated",
85 });
86 }
87
88 let count = u32::from_le_bytes(
89 data[0..GRAPH_TABLE_INDEX_HEADER_LEN]
90 .try_into()
91 .expect("header length checked"),
92 ) as usize;
93 let mut offset = GRAPH_TABLE_INDEX_HEADER_LEN;
94 let mut entries = Vec::with_capacity(count);
95
96 for _ in 0..count {
97 if data.len() < offset + GRAPH_TABLE_INDEX_ENTRY_HEADER_LEN {
98 return Err(GraphTableIndexFrameError::Malformed {
99 offset,
100 reason: "node id length truncated",
101 });
102 }
103 let id_len = u16::from_le_bytes(
104 data[offset..offset + GRAPH_TABLE_INDEX_ENTRY_HEADER_LEN]
105 .try_into()
106 .expect("entry header length checked"),
107 ) as usize;
108 offset += GRAPH_TABLE_INDEX_ENTRY_HEADER_LEN;
109
110 if data.len() < offset + id_len {
111 return Err(GraphTableIndexFrameError::Malformed {
112 offset,
113 reason: "node id bytes truncated",
114 });
115 }
116 let node_id = std::str::from_utf8(&data[offset..offset + id_len])
117 .map_err(|_| GraphTableIndexFrameError::Malformed {
118 offset,
119 reason: "node id not utf8",
120 })?
121 .to_string();
122 offset += id_len;
123
124 if data.len() < offset + GRAPH_TABLE_REF_SIZE {
125 return Err(GraphTableIndexFrameError::Malformed {
126 offset,
127 reason: "table ref truncated",
128 });
129 }
130 let table_ref = decode_graph_table_ref(&data[offset..offset + GRAPH_TABLE_REF_SIZE])
131 .ok_or(GraphTableIndexFrameError::Malformed {
132 offset,
133 reason: "invalid table ref",
134 })?;
135 offset += GRAPH_TABLE_REF_SIZE;
136
137 entries.push(GraphTableIndexEntry { node_id, table_ref });
138 }
139
140 Ok(entries)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn graph_table_index_frame_round_trips() {
149 let entries = vec![
150 GraphTableIndexEntry {
151 node_id: "node:a".to_string(),
152 table_ref: GraphTableRef {
153 table_id: 1,
154 row_id: 100,
155 },
156 },
157 GraphTableIndexEntry {
158 node_id: "node:b".to_string(),
159 table_ref: GraphTableRef {
160 table_id: 2,
161 row_id: 200,
162 },
163 },
164 ];
165
166 let encoded = encode_graph_table_index_frame(&entries).unwrap();
167 let decoded = decode_graph_table_index_frame(&encoded).unwrap();
168 assert_eq!(decoded, entries);
169 }
170
171 #[test]
172 fn graph_table_index_frame_rejects_truncated_input() {
173 assert!(matches!(
174 decode_graph_table_index_frame(&[1, 2, 3]),
175 Err(GraphTableIndexFrameError::Malformed { .. })
176 ));
177
178 let encoded = encode_graph_table_index_frame(&[GraphTableIndexEntry {
179 node_id: "node:a".to_string(),
180 table_ref: GraphTableRef {
181 table_id: 1,
182 row_id: 100,
183 },
184 }])
185 .unwrap();
186 assert!(matches!(
187 decode_graph_table_index_frame(&encoded[..encoded.len() - 1]),
188 Err(GraphTableIndexFrameError::Malformed { .. })
189 ));
190 }
191
192 #[test]
193 fn graph_table_index_frame_rejects_invalid_utf8_node_id() {
194 let mut bytes = Vec::new();
195 bytes.extend_from_slice(&1u32.to_le_bytes());
196 bytes.extend_from_slice(&1u16.to_le_bytes());
197 bytes.push(0xff);
198 bytes.extend_from_slice(&encode_graph_table_ref(GraphTableRef {
199 table_id: 1,
200 row_id: 100,
201 }));
202
203 assert!(matches!(
204 decode_graph_table_index_frame(&bytes),
205 Err(GraphTableIndexFrameError::Malformed { .. })
206 ));
207 }
208}