Skip to main content

qualia_client_core/api/
superblock.rs

1//! SuperBlock artifacts and views over unified v3 `.q42` volumes.
2
3#![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            // Empty roots still list; tiny non-volumes are skipped.
82            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}