1use std::collections::HashSet;
2
3use crate::{
4 error::MqdbError,
5 storage::page::{
6 PAGE_BODY_SIZE, PAGE_HEADER_SIZE, PAGE_TYPE_CATALOG, PageFile, make_page, parse_page_header,
7 },
8};
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct CatalogEntry {
12 pub document_id: u32,
13 pub path: Option<String>,
14 pub first_block_page: u32,
15 pub num_blocks: u32,
16 pub zone_map_bytes: Vec<u8>,
17 pub index_start_page: u32,
19}
20
21#[derive(Debug, Clone, PartialEq)]
28pub struct CustomTableEntry {
29 pub name: String,
30 pub columns: Vec<String>,
31 pub first_row_page: u32,
33 pub last_row_page: u32,
35 pub num_rows: u32,
36}
37
38fn invalid_data(message: impl Into<String>) -> MqdbError {
39 MqdbError::Storage(message.into())
40}
41
42fn as_u16(value: usize, field: &str) -> u16 {
43 u16::try_from(value).unwrap_or_else(|_| panic!("{field} exceeds u16 range"))
44}
45
46fn as_u32(value: usize, field: &str) -> u32 {
47 u32::try_from(value).unwrap_or_else(|_| panic!("{field} exceeds u32 range"))
48}
49
50struct Decoder<'a> {
51 data: &'a [u8],
52 pos: usize,
53}
54
55impl<'a> Decoder<'a> {
56 fn new(data: &'a [u8]) -> Self {
57 Self { data, pos: 0 }
58 }
59
60 fn read_exact(&mut self, len: usize) -> Result<&'a [u8], MqdbError> {
61 let end = self
62 .pos
63 .checked_add(len)
64 .ok_or_else(|| invalid_data("byte offset overflow"))?;
65 if end > self.data.len() {
66 return Err(invalid_data("unexpected end of catalog data"));
67 }
68 let bytes = &self.data[self.pos..end];
69 self.pos = end;
70 Ok(bytes)
71 }
72
73 fn read_u8(&mut self) -> Result<u8, MqdbError> {
74 Ok(self.read_exact(1)?[0])
75 }
76
77 fn read_u16(&mut self) -> Result<u16, MqdbError> {
78 let bytes: [u8; 2] = self
79 .read_exact(2)?
80 .try_into()
81 .map_err(|_| invalid_data("failed to read u16"))?;
82 Ok(u16::from_le_bytes(bytes))
83 }
84
85 fn read_u32(&mut self) -> Result<u32, MqdbError> {
86 let bytes: [u8; 4] = self
87 .read_exact(4)?
88 .try_into()
89 .map_err(|_| invalid_data("failed to read u32"))?;
90 Ok(u32::from_le_bytes(bytes))
91 }
92
93 fn read_string_u16(&mut self) -> Result<String, MqdbError> {
94 let len = usize::from(self.read_u16()?);
95 let bytes = self.read_exact(len)?;
96 String::from_utf8(bytes.to_vec())
97 .map_err(|e| invalid_data(format!("invalid catalog string UTF-8: {e}")))
98 }
99
100 fn remaining(&self) -> usize {
101 self.data.len() - self.pos
102 }
103}
104
105fn serialize_catalog(entries: &[CatalogEntry], custom_tables: &[CustomTableEntry]) -> Vec<u8> {
106 let mut out = Vec::new();
107 out.extend_from_slice(&as_u32(entries.len(), "catalog entry count").to_le_bytes());
108
109 for entry in entries {
110 out.extend_from_slice(&entry.document_id.to_le_bytes());
111 match &entry.path {
112 Some(path) => {
113 out.push(1);
114 out.extend_from_slice(&as_u16(path.len(), "catalog path length").to_le_bytes());
115 out.extend_from_slice(path.as_bytes());
116 }
117 None => out.push(0),
118 }
119 out.extend_from_slice(&entry.first_block_page.to_le_bytes());
120 out.extend_from_slice(&entry.num_blocks.to_le_bytes());
121 out.extend_from_slice(&as_u32(entry.zone_map_bytes.len(), "zone map length").to_le_bytes());
122 out.extend_from_slice(&entry.zone_map_bytes);
123 out.extend_from_slice(&entry.index_start_page.to_le_bytes());
124 }
125
126 out.extend_from_slice(&as_u32(custom_tables.len(), "custom table count").to_le_bytes());
127 for ct in custom_tables {
128 out.extend_from_slice(&as_u16(ct.name.len(), "table name length").to_le_bytes());
129 out.extend_from_slice(ct.name.as_bytes());
130 out.extend_from_slice(&as_u16(ct.columns.len(), "column count").to_le_bytes());
131 for col in &ct.columns {
132 out.extend_from_slice(&as_u16(col.len(), "column name length").to_le_bytes());
133 out.extend_from_slice(col.as_bytes());
134 }
135 out.extend_from_slice(&ct.first_row_page.to_le_bytes());
136 out.extend_from_slice(&ct.last_row_page.to_le_bytes());
137 out.extend_from_slice(&ct.num_rows.to_le_bytes());
138 }
139
140 out
141}
142
143pub fn write_catalog(
144 pf: &mut PageFile,
145 entries: &[CatalogEntry],
146 custom_tables: &[CustomTableEntry],
147) -> Result<(), MqdbError> {
148 if pf.num_pages < 2 {
149 return Err(invalid_data("catalog start page is missing"));
150 }
151
152 let bytes = serialize_catalog(entries, custom_tables);
153 let chunks: Vec<&[u8]> = if bytes.is_empty() {
154 vec![&[]]
155 } else {
156 bytes.chunks(PAGE_BODY_SIZE).collect()
157 };
158
159 let mut page_ids = Vec::with_capacity(chunks.len());
160 page_ids.push(1);
161
162 for _ in 1..chunks.len() {
163 let placeholder = make_page(PAGE_TYPE_CATALOG, 0, 0, &[]);
164 let page_id = pf.append_page(&placeholder)?;
165 page_ids.push(page_id);
166 }
167
168 for (index, chunk) in chunks.iter().enumerate() {
169 let page_id = page_ids[index];
170 let next_page = page_ids.get(index + 1).copied().unwrap_or(0);
171 let page = make_page(PAGE_TYPE_CATALOG, page_id, next_page, chunk);
172 pf.write_page(page_id, &page)?;
173 }
174
175 Ok(())
176}
177
178pub fn read_catalog(
179 pf: &mut PageFile,
180) -> Result<(Vec<CatalogEntry>, Vec<CustomTableEntry>), MqdbError> {
181 if pf.num_pages < 2 {
182 return Err(invalid_data("catalog start page is missing"));
183 }
184
185 let mut bytes = Vec::new();
186 let mut page_id = 1u32;
187 let mut visited = HashSet::new();
188
189 loop {
190 if !visited.insert(page_id) {
191 return Err(invalid_data("catalog page chain contains a cycle"));
192 }
193
194 let page = pf.read_page(page_id)?;
195 let (page_type, _, stored_page_id, next_page) = parse_page_header(&page);
196 if page_type != PAGE_TYPE_CATALOG {
197 return Err(invalid_data(format!(
198 "page {page_id} is not a catalog page"
199 )));
200 }
201 if stored_page_id != page_id {
202 return Err(invalid_data(format!(
203 "catalog page header mismatch: expected {page_id}, found {stored_page_id}"
204 )));
205 }
206
207 bytes.extend_from_slice(&page[PAGE_HEADER_SIZE..]);
208
209 if next_page == 0 {
210 break;
211 }
212 page_id = next_page;
213 }
214
215 let mut decoder = Decoder::new(&bytes);
216 let entry_count = usize::try_from(decoder.read_u32()?)
217 .map_err(|_| invalid_data("catalog entry count exceeds usize range"))?;
218 let mut entries = Vec::with_capacity(entry_count);
219
220 for _ in 0..entry_count {
221 let document_id = decoder.read_u32()?;
222 let path = match decoder.read_u8()? {
223 0 => None,
224 1 => Some(decoder.read_string_u16()?),
225 value => return Err(invalid_data(format!("invalid path presence tag: {value}"))),
226 };
227 let first_block_page = decoder.read_u32()?;
228 let num_blocks = decoder.read_u32()?;
229 let zone_map_len = usize::try_from(decoder.read_u32()?)
230 .map_err(|_| invalid_data("zone map length exceeds usize range"))?;
231 let zone_map_bytes = decoder.read_exact(zone_map_len)?.to_vec();
232
233 let index_start_page = decoder.read_u32()?;
234 entries.push(CatalogEntry {
235 document_id,
236 path,
237 first_block_page,
238 num_blocks,
239 zone_map_bytes,
240 index_start_page,
241 });
242 }
243
244 let custom_tables = if decoder.remaining() >= 4 {
245 let count = usize::try_from(decoder.read_u32()?)
246 .map_err(|_| invalid_data("custom table count exceeds usize range"))?;
247 let mut tables = Vec::with_capacity(count);
248 for _ in 0..count {
249 let name = decoder.read_string_u16()?;
250 let num_cols = usize::from(decoder.read_u16()?);
251 let mut columns = Vec::with_capacity(num_cols);
252 for _ in 0..num_cols {
253 columns.push(decoder.read_string_u16()?);
254 }
255 let first_row_page = decoder.read_u32()?;
256 let last_row_page = decoder.read_u32()?;
257 let num_rows = decoder.read_u32()?;
258 tables.push(CustomTableEntry {
259 name,
260 columns,
261 first_row_page,
262 last_row_page,
263 num_rows,
264 });
265 }
266 tables
267 } else {
268 vec![]
269 };
270
271 Ok((entries, custom_tables))
272}