1use crate::EseError;
17
18fn le_u32_at(data: &[u8], off: usize) -> u32 {
24 let mut b = [0u8; 4];
25 if let Some(s) = data.get(off..off + 4) {
26 b.copy_from_slice(s);
27 }
28 u32::from_le_bytes(b)
29}
30
31#[derive(Debug, Clone)]
33pub struct CatalogEntry {
34 pub object_type: u16,
36 pub object_id: u32,
38 pub parent_object_id: u32,
40 pub table_page: u32,
42 pub object_name: String,
44}
45
46impl CatalogEntry {
47 pub const MIN_SIZE: usize = 16;
49
50 pub fn from_bytes(data: &[u8]) -> Result<Self, EseError> {
57 if data.len() < Self::MIN_SIZE {
58 return Err(EseError::Corrupt {
59 page: 0,
60 detail: format!(
61 "catalog record too short: {} < {}",
62 data.len(),
63 Self::MIN_SIZE
64 ),
65 });
66 }
67 let object_type = u16::from_le_bytes([data[0], data[1]]);
68 let object_id = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
69 let parent_object_id = u32::from_le_bytes([data[6], data[7], data[8], data[9]]);
70 let table_page = u32::from_le_bytes([data[10], data[11], data[12], data[13]]);
71 let name_len = u16::from_le_bytes([data[14], data[15]]) as usize;
72 if data.len() < Self::MIN_SIZE + name_len {
73 return Err(EseError::Corrupt {
74 page: 0,
75 detail: format!(
76 "catalog record name truncated: need {}, got {}",
77 Self::MIN_SIZE + name_len,
78 data.len()
79 ),
80 });
81 }
82 let name_bytes = &data[16..16 + name_len];
83 let object_name = std::str::from_utf8(name_bytes)
84 .map_err(|e| EseError::Corrupt {
85 page: 0,
86 detail: format!("catalog name not UTF-8: {e}"),
87 })?
88 .to_owned();
89 Ok(Self {
90 object_type,
91 object_id,
92 parent_object_id,
93 table_page,
94 object_name,
95 })
96 }
97
98 pub fn to_bytes(&self) -> Vec<u8> {
100 let name_bytes = self.object_name.as_bytes();
101 let mut out = Vec::with_capacity(Self::MIN_SIZE + name_bytes.len());
102 out.extend_from_slice(&self.object_type.to_le_bytes());
103 out.extend_from_slice(&self.object_id.to_le_bytes());
104 out.extend_from_slice(&self.parent_object_id.to_le_bytes());
105 out.extend_from_slice(&self.table_page.to_le_bytes());
106 out.extend_from_slice(&(u16::try_from(name_bytes.len()).unwrap_or(u16::MAX)).to_le_bytes());
107 out.extend_from_slice(name_bytes);
108 out
109 }
110
111 pub fn scan_catalog_page_data(data_area: &[u8]) -> Vec<Self> {
128 const MIN_I: usize = 20; const MAX_NAME: usize = 64;
130 let len = data_area.len();
131 let mut entries: Vec<Self> = Vec::new();
132 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
133 let mut i = MIN_I;
134 while i + 4 <= len {
135 if data_area[i] != 0xff || data_area[i + 1] != 0x00 {
136 i += 1;
137 continue;
138 }
139 let name_len = u16::from_le_bytes([data_area[i + 2], data_area[i + 3]]) as usize;
140 if name_len == 0 || name_len > MAX_NAME || i + 4 + name_len > len {
141 i += 1;
142 continue;
143 }
144 let name_bytes = &data_area[i + 4..i + 4 + name_len];
145 if !name_bytes.is_ascii() {
146 i += 1;
147 continue;
148 }
149 let Ok(name) = std::str::from_utf8(name_bytes) else {
150 i += 1;
151 continue;
152 };
153 if name.is_empty() || seen.contains(name) {
154 i += 1;
155 continue;
156 }
157 let pgnofdf_raw = le_u32_at(data_area, i - 16);
161 let object_id = le_u32_at(data_area, i - 20);
162 seen.insert(name);
163 entries.push(Self {
164 object_type: 1,
165 object_id,
166 parent_object_id: 1,
167 table_page: pgnofdf_raw + 1,
168 object_name: name.to_owned(),
169 });
170 i += 4 + name_len;
171 }
172 entries
173 }
174
175 pub fn parse_real_catalog_record(data: &[u8]) -> Option<Self> {
188 const MIN_BEFORE: usize = 20; let len = data.len();
190 let mut i = MIN_BEFORE;
191 while i + 4 <= len {
192 if data[i] != 0xff || data[i + 1] != 0x00 {
193 i += 1;
194 continue;
195 }
196 let name_len = u16::from_le_bytes([data[i + 2], data[i + 3]]) as usize;
197 if name_len == 0 || i + 4 + name_len > len {
198 i += 1;
199 continue;
200 }
201 let name_bytes = &data[i + 4..i + 4 + name_len];
202 if !name_bytes.is_ascii() {
203 i += 1;
204 continue;
205 }
206 let Ok(name) = std::str::from_utf8(name_bytes) else {
207 i += 1;
208 continue;
209 };
210 if name.is_empty() {
211 i += 1;
212 continue;
213 }
214 let pgnofdf_raw = u32::from_le_bytes(data[i - 16..i - 12].try_into().ok()?);
215 let object_id = u32::from_le_bytes(data[i - 20..i - 16].try_into().ok()?);
216 let table_page = pgnofdf_raw + 1; return Some(Self {
218 object_type: 1,
219 object_id,
220 parent_object_id: 1,
221 table_page,
222 object_name: name.to_owned(),
223 });
224 }
225 None
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn catalog_entry_roundtrip() {
235 let entry = CatalogEntry {
236 object_type: 1,
237 object_id: 10,
238 parent_object_id: 1,
239 table_page: 42,
240 object_name: "SruDbNetworkTable".to_owned(),
241 };
242 let bytes = entry.to_bytes();
243 let parsed = CatalogEntry::from_bytes(&bytes).expect("parse");
244 assert_eq!(parsed.object_name, "SruDbNetworkTable");
245 assert_eq!(parsed.table_page, 42);
246 assert_eq!(parsed.object_type, 1);
247 }
248
249 #[test]
250 fn catalog_entry_too_short_returns_err() {
251 let result = CatalogEntry::from_bytes(&[0u8; 5]);
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn parse_real_catalog_record_extracts_name_and_page() {
257 let object_id: u32 = 42;
261 let pgnofdf_raw: u32 = 31; let name = b"SruDbIdMapTable";
263 let name_len = name.len() as u16;
264
265 let mut data = vec![0u8; 20 + 4 + name.len()];
266 data[0..4].copy_from_slice(&object_id.to_le_bytes());
268 data[4..8].copy_from_slice(&pgnofdf_raw.to_le_bytes());
270 data[20] = 0xff;
273 data[21] = 0x00;
274 data[22..24].copy_from_slice(&name_len.to_le_bytes());
275 data[24..24 + name.len()].copy_from_slice(name);
276
277 let entry = CatalogEntry::parse_real_catalog_record(&data).expect("must find TABLE entry");
278 assert_eq!(entry.object_name, "SruDbIdMapTable");
279 assert_eq!(entry.table_page, 32); assert_eq!(entry.object_id, 42);
281 assert_eq!(entry.object_type, 1);
282 }
283
284 #[test]
285 fn parse_real_catalog_record_returns_none_for_synthetic_format() {
286 let entry = CatalogEntry {
289 object_type: 1,
290 object_id: 2,
291 parent_object_id: 1,
292 table_page: 100,
293 object_name: "OrphanedTable".to_owned(),
294 };
295 let bytes = entry.to_bytes();
296 assert!(
297 CatalogEntry::parse_real_catalog_record(&bytes).is_none(),
298 "synthetic format must not match real catalog scanner"
299 );
300 }
301}