1#![allow(non_snake_case)]
4
5use serde::Serialize;
6use std::fs;
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10use qualia_core_db::q42_volume::{
11 decode_superblock_quins, is_unified_volume, Q42Volume, SUPERBLOCK_SIZE,
12};
13use qualia_core_db::q42_reader::read_c_q42_quins;
14
15#[derive(Debug, Clone, Serialize)]
16pub struct SuperBlockArtifact {
17 pub path: String,
18 pub display_name: String,
19 pub byte_size: u64,
20 pub block_count: u64,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct SuperBlockView {
25 pub source_path: String,
26 pub block_index: u64,
27 pub total_blocks: u64,
28 pub block_sequence_id: u64,
29 pub storage_owner_did: u64,
30 pub active_quin_count: u64,
31 pub validation_checksum: u32,
32 pub hardware_profile_flags: u32,
33 pub fea_mesh_index_id: u64,
34 pub raw_bytes: Vec<u8>,
35 pub quins: Vec<[u64; 6]>,
36}
37
38fn decode_u64(bytes: &[u8], start: usize) -> Result<u64, String> {
39 let end = start + 8;
40 let slice = bytes
41 .get(start..end)
42 .ok_or_else(|| format!("SuperBlock truncated at byte range {start}..{end}"))?;
43 let mut buf = [0u8; 8];
44 buf.copy_from_slice(slice);
45 Ok(u64::from_le_bytes(buf))
46}
47
48fn decode_u32(bytes: &[u8], start: usize) -> Result<u32, String> {
49 let end = start + 4;
50 let slice = bytes
51 .get(start..end)
52 .ok_or_else(|| format!("SuperBlock truncated at byte range {start}..{end}"))?;
53 let mut buf = [0u8; 4];
54 buf.copy_from_slice(slice);
55 Ok(u32::from_le_bytes(buf))
56}
57
58fn scan_q42_artifacts(root: &Path, out: &mut Vec<SuperBlockArtifact>) -> Result<(), String> {
59 if !root.is_dir() {
60 return Ok(());
61 }
62 for entry in fs::read_dir(root).map_err(|e| e.to_string())? {
63 let entry = entry.map_err(|e| e.to_string())?;
64 let path = entry.path();
65 if path.is_dir() {
66 scan_q42_artifacts(&path, out)?;
67 continue;
68 }
69 if path.extension().and_then(|v| v.to_str()) != Some("q42") {
70 continue;
71 }
72 let Some(name) = path.file_name().and_then(|v| v.to_str()) else {
73 continue;
74 };
75 if name.ends_with(".c.q42") {
76 continue;
77 }
78 let meta = entry.metadata().map_err(|e| e.to_string())?;
79 let block_count = artifact_block_count(&path, meta.len())?;
80 if block_count == 0 && meta.len() < SUPERBLOCK_SIZE as u64 {
81 if is_unified_volume(&path).ok() != Some(true) {
83 continue;
84 }
85 }
86 out.push(SuperBlockArtifact {
87 path: path.to_string_lossy().into_owned(),
88 display_name: name.to_string(),
89 byte_size: meta.len(),
90 block_count,
91 });
92 }
93 Ok(())
94}
95
96fn artifact_block_count(path: &Path, byte_len: u64) -> Result<u64, String> {
97 if is_unified_volume(path).ok() == Some(true) {
98 let volume = Q42Volume::open(path).map_err(|e| e.to_string())?;
99 return Ok(volume.block_count());
100 }
101 if byte_len >= SUPERBLOCK_SIZE as u64 && byte_len % SUPERBLOCK_SIZE as u64 == 0 {
102 return Ok(byte_len / SUPERBLOCK_SIZE as u64);
103 }
104 if byte_len >= 16 {
105 return Ok(1);
106 }
107 Ok(0)
108}
109
110pub fn list_superblock_artifacts() -> Result<Vec<SuperBlockArtifact>, String> {
111 let state = crate::state::APP_STATE.get().unwrap();
112 let storage = state.config.lock().unwrap().storage_path.clone();
113 let mut out = Vec::new();
114 scan_q42_artifacts(Path::new(&storage), &mut out)?;
115 out.sort_by(|a, b| {
116 a.display_name
117 .cmp(&b.display_name)
118 .then_with(|| a.path.cmp(&b.path))
119 });
120 Ok(out)
121}
122
123pub fn get_superblock_view(
124 source_path: String,
125 block_index: u64,
126) -> Result<SuperBlockView, String> {
127 let path = PathBuf::from(&source_path);
128 if !path.is_file() {
129 return Err(format!("SuperBlock source not found: {}", path.display()));
130 }
131 if path.extension().and_then(|v| v.to_str()) != Some("q42") {
132 return Err("Block inspector expects a raw .q42 artifact".to_string());
133 }
134
135 if is_unified_volume(&path).ok() == Some(true) {
136 return view_unified_volume(&path, source_path, block_index);
137 }
138
139 let metadata = fs::metadata(&path).map_err(|e| e.to_string())?;
140 if metadata.len() >= SUPERBLOCK_SIZE as u64 && metadata.len() % SUPERBLOCK_SIZE as u64 == 0 {
141 return view_legacy_raw_pages(&path, source_path, block_index, metadata.len());
142 }
143
144 view_legacy_framed(&path, source_path, block_index)
145}
146
147fn view_unified_volume(
148 path: &Path,
149 source_path: String,
150 block_index: u64,
151) -> Result<SuperBlockView, String> {
152 let volume = Q42Volume::open(path).map_err(|e| e.to_string())?;
153 if volume
154 .volume_manifest()
155 .map_err(|e| e.to_string())?
156 .is_some()
157 {
158 return Err(
159 "This Q42 is a volume-set root (no local SuperBlocks). Open a data child segment."
160 .into(),
161 );
162 }
163 let total_blocks = volume.block_count();
164 if total_blocks == 0 {
165 return Err("Volume has no SuperBlocks".into());
166 }
167 if block_index >= total_blocks {
168 return Err(format!(
169 "Block index {block_index} is out of range for {total_blocks} blocks"
170 ));
171 }
172 let mut raw_bytes = vec![0u8; SUPERBLOCK_SIZE];
173 volume
174 .read_superblock_into(block_index as usize, &mut raw_bytes)
175 .map_err(|e| e.to_string())?;
176 view_from_decompressed(source_path, block_index, total_blocks, raw_bytes)
177}
178
179fn view_legacy_raw_pages(
180 path: &Path,
181 source_path: String,
182 block_index: u64,
183 file_len: u64,
184) -> Result<SuperBlockView, String> {
185 let total_blocks = file_len / SUPERBLOCK_SIZE as u64;
186 if block_index >= total_blocks {
187 return Err(format!(
188 "Block index {block_index} is out of range for {total_blocks} blocks"
189 ));
190 }
191 let mut file = fs::File::open(path).map_err(|e| e.to_string())?;
192 use std::io::Seek;
193 use std::io::SeekFrom;
194 file.seek(SeekFrom::Start(block_index * SUPERBLOCK_SIZE as u64))
195 .map_err(|e| e.to_string())?;
196 let mut raw_bytes = vec![0u8; SUPERBLOCK_SIZE];
197 file.read_exact(&mut raw_bytes).map_err(|e| e.to_string())?;
198 view_from_decompressed(source_path, block_index, total_blocks, raw_bytes)
199}
200
201fn view_legacy_framed(
202 path: &Path,
203 source_path: String,
204 block_index: u64,
205) -> Result<SuperBlockView, String> {
206 if block_index != 0 {
207 return Err("Legacy framed .q42 exposes a single logical block (index 0)".into());
208 }
209 let quins = read_c_q42_quins(path).map_err(|e| e.to_string())?;
210 if quins.is_empty() {
211 return Err("Legacy framed .q42 contained no Quins".into());
212 }
213 let packed: Vec<[u64; 6]> = quins
214 .iter()
215 .map(|q| {
216 [
217 q.subject,
218 q.predicate,
219 q.object,
220 q.context,
221 q.metadata,
222 q.parity,
223 ]
224 })
225 .collect();
226 Ok(SuperBlockView {
227 source_path,
228 block_index: 0,
229 total_blocks: 1,
230 block_sequence_id: 0,
231 storage_owner_did: 0,
232 active_quin_count: packed.len() as u64,
233 validation_checksum: 0,
234 hardware_profile_flags: 0,
235 fea_mesh_index_id: 0,
236 raw_bytes: Vec::new(),
237 quins: packed,
238 })
239}
240
241fn view_from_decompressed(
242 source_path: String,
243 block_index: u64,
244 total_blocks: u64,
245 raw_bytes: Vec<u8>,
246) -> Result<SuperBlockView, String> {
247 let block_sequence_id = decode_u64(&raw_bytes, 0)?;
248 let storage_owner_did = decode_u64(&raw_bytes, 8)?;
249 let decoded = decode_superblock_quins(&raw_bytes).map_err(|e| e.to_string())?;
250 let active_quin_count = decoded.len() as u64;
251 let validation_checksum = decode_u32(&raw_bytes, 24)?;
252 let hardware_profile_flags = decode_u32(&raw_bytes, 28)?;
253 let fea_mesh_index_id = decode_u64(&raw_bytes, 32)?;
254 let quins = decoded
255 .into_iter()
256 .map(|q| {
257 [
258 q.subject,
259 q.predicate,
260 q.object,
261 q.context,
262 q.metadata,
263 q.parity,
264 ]
265 })
266 .collect();
267 Ok(SuperBlockView {
268 source_path,
269 block_index,
270 total_blocks,
271 block_sequence_id,
272 storage_owner_did,
273 active_quin_count,
274 validation_checksum,
275 hardware_profile_flags,
276 fea_mesh_index_id,
277 raw_bytes,
278 quins,
279 })
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use qualia_core_db::q42_volume::write_sorted_quins_volume;
286 use qualia_core_db::NQuin;
287
288 fn sample_quin(object: u64) -> NQuin {
289 NQuin {
290 subject: 11,
291 predicate: 22,
292 object,
293 context: 33,
294 metadata: 0,
295 parity: NQuin::calculate_parity(11, 22, object, 33, 0),
296 }
297 }
298
299 #[test]
300 fn inspector_reads_unified_v3_block() {
301 let dir = tempfile::tempdir().unwrap();
302 let path = dir.path().join("body.q42");
303 write_sorted_quins_volume(&path, &[sample_quin(7), sample_quin(3)]).unwrap();
304 let view = get_superblock_view(path.to_string_lossy().into_owned(), 0).unwrap();
305 assert_eq!(view.total_blocks, 1);
306 assert_eq!(view.active_quin_count, 2);
307 assert_eq!(view.quins.len(), 2);
308 assert!(view.quins.iter().any(|q| q[2] == 3));
309 assert!(view.quins.iter().any(|q| q[2] == 7));
310 }
311}