qualia_client_core/
q42_compress.rs1use std::fs;
7use std::io::{Read, Write};
8use std::path::Path;
9
10use serde::Serialize;
11
12const CHUNK_SIZE: usize = 393_216;
13
14#[derive(Debug, Clone, Serialize)]
15pub struct CompressStats {
16 pub input_bytes: u64,
17 pub output_bytes: u64,
18 pub blocks: u64,
19 pub ratio: f64,
20}
21
22pub fn finalize_c_q42(q42_path: &Path, c_q42_path: &Path) -> Result<CompressStats, String> {
24 if !q42_path.is_file() {
25 return Err(format!("Missing .q42 artifact: {}", q42_path.display()));
26 }
27 if let Some(parent) = c_q42_path.parent() {
28 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
29 }
30 fs::copy(q42_path, c_q42_path).map_err(|e| e.to_string())?;
31 let input_bytes = fs::metadata(q42_path).map_err(|e| e.to_string())?.len();
32 let output_bytes = fs::metadata(c_q42_path).map_err(|e| e.to_string())?.len();
33 let blocks = if qualia_core_db::q42_volume::is_unified_volume(q42_path).unwrap_or(false) {
34 qualia_core_db::q42_volume::Q42Volume::open(q42_path)
35 .map(|v| v.block_count())
36 .unwrap_or(1)
37 } else {
38 output_bytes.saturating_div(CHUNK_SIZE as u64).max(1)
39 };
40 Ok(CompressStats {
41 input_bytes,
42 output_bytes,
43 blocks,
44 ratio: if output_bytes > 0 {
45 input_bytes as f64 / output_bytes as f64
46 } else {
47 1.0
48 },
49 })
50}
51
52pub fn compress_raw_file(input: &Path, output: &Path) -> Result<CompressStats, String> {
54 let input_bytes = fs::metadata(input).map_err(|e| e.to_string())?.len();
55 let mut reader = fs::File::open(input).map_err(|e| e.to_string())?;
56 let mut writer = fs::File::create(output).map_err(|e| e.to_string())?;
57 let mut buf = vec![0u8; CHUNK_SIZE];
58 let mut block_id: u64 = 0;
59 loop {
60 let n = read_partial(&mut reader, &mut buf).map_err(|e| e.to_string())?;
61 if n == 0 {
62 break;
63 }
64 block_id = write_lz4_block(&mut writer, block_id, &buf[..n]).map_err(|e| e.to_string())?;
65 }
66 writer.flush().map_err(|e| e.to_string())?;
67 let output_bytes = fs::metadata(output).map_err(|e| e.to_string())?.len();
68 Ok(CompressStats {
69 input_bytes,
70 output_bytes,
71 blocks: block_id,
72 ratio: if output_bytes > 0 {
73 input_bytes as f64 / output_bytes as f64
74 } else {
75 1.0
76 },
77 })
78}
79
80fn write_lz4_block(w: &mut impl Write, block_id: u64, data: &[u8]) -> std::io::Result<u64> {
81 let compressed = lz4_flex::compress_prepend_size(data);
82 w.write_all(&block_id.to_le_bytes())?;
83 w.write_all(&(compressed.len() as u32).to_le_bytes())?;
84 w.write_all(&(data.len() as u32).to_le_bytes())?;
85 w.write_all(&compressed)?;
86 Ok(block_id + 1)
87}
88
89fn read_partial(r: &mut impl Read, buf: &mut [u8]) -> std::io::Result<usize> {
90 let mut total = 0;
91 while total < buf.len() {
92 match r.read(&mut buf[total..])? {
93 0 => break,
94 n => total += n,
95 }
96 }
97 Ok(total)
98}