1use crate::error::PrivacyError;
28use crate::types::FiveDimCoord;
29use sha2::{Digest, Sha256};
30use serde::{Deserialize, Serialize};
31
32extern crate alloc;
33use alloc::{format, vec::Vec};
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AffineTransform {
38 pub a: [f64; 25],
40 pub b: [f64; 5],
42 pub prob: f64,
44}
45
46impl AffineTransform {
47 pub fn identity() -> Self {
49 let mut a = [0.0f64; 25];
50 for i in 0..5 { a[i * 5 + i] = 1.0; }
51 Self { a, b: [0.0; 5], prob: 0.2 }
52 }
53
54 #[allow(clippy::needless_range_loop)] pub fn apply(&self, x: &[f64; 5]) -> [f64; 5] {
57 let mut result = [0.0f64; 5];
58 for i in 0..5 {
59 for j in 0..5 {
60 result[i] += self.a[i * 5 + j] * x[j];
61 }
62 result[i] += self.b[i];
63 }
64 result
65 }
66
67 pub fn apply_inverse_1d(&self, y: f64, dim: usize) -> f64 {
72 let a_diag = self.a[dim * 5 + dim];
73 if a_diag.abs() < 1e-12 {
74 0.0
75 } else {
76 (y - self.b[dim]) / a_diag
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct IfsCodebook {
86 pub transforms: Vec<AffineTransform>,
87 pub dims_reduced: usize,
88 pub chaos_seed: [u8; 32],
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct CompressedBlob {
99 pub payload: Vec<u8>,
101 pub stored: bool,
103 pub original_size: usize,
105 pub original_hash: Vec<u8>,
107}
108
109impl CompressedBlob {
110 pub fn ratio(&self) -> f64 {
115 if self.original_size == 0 {
116 return 1.0;
117 }
118 self.payload.len() as f64 / self.original_size as f64
119 }
120}
121
122pub struct FractalCompressor;
124
125impl FractalCompressor {
126 pub fn new() -> Self {
127 Self
128 }
129
130 #[allow(clippy::needless_range_loop)] pub fn compress(
136 &self,
137 coord: &FiveDimCoord,
138 chaos_seed: &[u8; 32],
139 target_dims: usize,
140 ) -> Result<IfsCodebook, PrivacyError> {
141 if target_dims == 0 || target_dims > 5 {
142 return Err(PrivacyError::CompressionFailed(
143 format!("target_dims must be 1–5, got {target_dims}")
144 ));
145 }
146
147 let x = [
148 coord.spatial,
149 coord.temporal,
150 coord.probabilistic,
151 coord.quantum,
152 coord.chaotic,
153 ];
154
155 let mut transforms = Vec::new();
157 for i in 0..target_dims {
158 let mut t = AffineTransform::identity();
159
160 let delta = self.chaos_delta(chaos_seed, i as u64);
162 for j in 0..25 {
163 t.a[j] *= 0.5 + delta * 0.1; }
165 t.b[i] = x[i] * (1.0 - 0.5 - delta * 0.1);
166 t.prob = 1.0 / target_dims as f64;
167
168 transforms.push(t);
169 }
170
171 Ok(IfsCodebook {
172 transforms,
173 dims_reduced: 5 - target_dims,
174 chaos_seed: *chaos_seed,
175 })
176 }
177
178 pub fn compress_bytes(&self, data: &[u8]) -> Result<CompressedBlob, PrivacyError> {
190 if data.is_empty() {
191 return Err(PrivacyError::CompressionFailed("Empty data".into()));
192 }
193
194 let original_hash = Sha256::digest(data).to_vec();
195 let deflated = miniz_oxide::deflate::compress_to_vec(data, 6);
196
197 let (payload, stored) = if deflated.len() < data.len() {
199 (deflated, false)
200 } else {
201 (data.to_vec(), true)
202 };
203
204 Ok(CompressedBlob {
205 payload,
206 stored,
207 original_size: data.len(),
208 original_hash,
209 })
210 }
211
212 pub fn decompress(&self, codebook: &IfsCodebook) -> Result<FiveDimCoord, PrivacyError> {
216 if codebook.transforms.is_empty() {
217 return Err(PrivacyError::DecompressionFailed("Empty codebook".into()));
218 }
219
220 let mut x = [0.0f64; 5];
222 for step in 0..100u64 {
223 let t_idx = (step as usize) % codebook.transforms.len();
224 x = codebook.transforms[t_idx].apply(&x);
225 }
226
227 Ok(FiveDimCoord {
228 spatial: x[0],
229 temporal: x[1],
230 probabilistic: x[2],
231 quantum: x[3],
232 chaotic: x[4],
233 })
234 }
235
236 pub fn decompress_bytes(blob: &CompressedBlob) -> Result<Vec<u8>, PrivacyError> {
243 let result = if blob.stored {
244 blob.payload.clone()
245 } else {
246 miniz_oxide::inflate::decompress_to_vec(&blob.payload)
247 .map_err(|e| PrivacyError::DecompressionFailed(format!("inflate failed: {e:?}")))?
248 };
249
250 if result.len() != blob.original_size {
251 return Err(PrivacyError::DecompressionFailed(format!(
252 "length mismatch: recovered {} bytes, expected {}",
253 result.len(),
254 blob.original_size
255 )));
256 }
257
258 if Sha256::digest(&result).to_vec() != blob.original_hash {
259 return Err(PrivacyError::DecompressionFailed(
260 "hash mismatch: data corrupted".into(),
261 ));
262 }
263
264 Ok(result)
265 }
266
267 pub fn compress_and_verify(
275 &self,
276 data: &[u8],
277 ) -> Result<(CompressedBlob, Vec<u8>), PrivacyError> {
278 let blob = self.compress_bytes(data)?;
279 let decompressed = Self::decompress_bytes(&blob)?;
280
281 if decompressed != data {
282 return Err(PrivacyError::DecompressionFailed(
283 "Round-trip verification failed: decompressed data does not match original".into()
284 ));
285 }
286
287 Ok((blob, decompressed))
288 }
289
290 pub fn compression_ratio(original_dims: usize, codebook: &IfsCodebook) -> f64 {
296 let original_size = original_dims * 8; let compressed_size = codebook.transforms.len() * (25 + 5 + 1) * 8;
298 original_size as f64 / compressed_size as f64
299 }
300
301 fn chaos_delta(&self, chaos_seed: &[u8; 32], counter: u64) -> f64 {
302 let mut hasher = Sha256::new();
303 hasher.update(chaos_seed);
304 hasher.update(counter.to_le_bytes());
305 hasher.update(b"ifs-delta");
306 let h: [u8; 32] = hasher.finalize().into();
307 let raw = u64::from_le_bytes(h[..8].try_into().unwrap_or([0u8; 8]));
308 (raw as f64 / u64::MAX as f64) * 2.0 - 1.0 }
310}
311
312impl Default for FractalCompressor {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318pub fn reduce_5d_to_3d(coords: &[FiveDimCoord]) -> Vec<[f64; 3]> {
326 if coords.is_empty() {
327 return Vec::new();
328 }
329
330 let n = coords.len() as f64;
332 let dims: [[f64; 5]; 1] = [[0.0; 5]]; let _ = dims;
334
335 let all_dims: Vec<[f64; 5]> = coords.iter().map(|c| [
337 c.spatial,
338 c.temporal,
339 c.probabilistic,
340 c.quantum,
341 c.chaotic,
342 ]).collect();
343
344 let mut means = [0.0f64; 5];
346 for pt in &all_dims {
347 for d in 0..5 {
348 means[d] += pt[d];
349 }
350 }
351 for m in means.iter_mut() {
352 *m /= n;
353 }
354
355 let mut variances = [0.0f64; 5];
357 for pt in &all_dims {
358 for d in 0..5 {
359 let diff = pt[d] - means[d];
360 variances[d] += diff * diff;
361 }
362 }
363 for v in variances.iter_mut() {
364 *v /= n;
365 }
366
367 let mut dim_indices: [usize; 5] = [0, 1, 2, 3, 4];
369 for i in 1..5 {
371 let mut j = i;
372 while j > 0 && variances[dim_indices[j]] > variances[dim_indices[j - 1]] {
373 dim_indices.swap(j, j - 1);
374 j -= 1;
375 }
376 }
377 let top3 = [dim_indices[0], dim_indices[1], dim_indices[2]];
378
379 all_dims.iter().map(|pt| [pt[top3[0]], pt[top3[1]], pt[top3[2]]]).collect()
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_compress_decompress() {
389 let compressor = FractalCompressor::new();
390 let seed = [0u8; 32];
391 let coord = FiveDimCoord {
392 spatial: 0.5, temporal: 1000.0, probabilistic: 1e-6,
393 quantum: 1.2, chaotic: 0.8,
394 };
395 let codebook = compressor.compress(&coord, &seed, 3).unwrap();
396 assert_eq!(codebook.dims_reduced, 2);
397 let decompressed = compressor.decompress(&codebook).unwrap();
398 assert!(decompressed.spatial.is_finite());
399 }
400
401 #[test]
402 fn test_invalid_dims() {
403 let compressor = FractalCompressor::new();
404 let seed = [0u8; 32];
405 let coord = FiveDimCoord::zero();
406 assert!(compressor.compress(&coord, &seed, 0).is_err());
407 assert!(compressor.compress(&coord, &seed, 6).is_err());
408 }
409
410 #[test]
411 fn test_compress_bytes_round_trip() {
412 let compressor = FractalCompressor::new();
413 let data = b"Hello, compression world! This is a test of lossless byte compression.";
414 let blob = compressor.compress_bytes(data).unwrap();
415 assert_eq!(blob.original_size, data.len());
416 let decompressed = FractalCompressor::decompress_bytes(&blob).unwrap();
417 assert_eq!(decompressed, data);
418 }
419
420 #[test]
423 fn test_compressible_input_actually_shrinks() {
424 let compressor = FractalCompressor::new();
425 let data = alloc::vec![b'A'; 4096];
426 let blob = compressor.compress_bytes(&data).unwrap();
427
428 assert!(!blob.stored, "repetitive input should compress, not fall back to stored");
429 assert!(
430 blob.payload.len() < data.len(),
431 "payload {} not smaller than input {}",
432 blob.payload.len(),
433 data.len()
434 );
435 assert!(blob.ratio() < 1.0);
436 assert_eq!(FractalCompressor::decompress_bytes(&blob).unwrap(), data);
437 }
438
439 #[test]
441 fn test_incompressible_input_falls_back_to_stored() {
442 let compressor = FractalCompressor::new();
443 let mut data = Vec::new();
445 for i in 0..64u32 {
446 data.extend_from_slice(&Sha256::digest(i.to_le_bytes()));
447 }
448
449 let blob = compressor.compress_bytes(&data).unwrap();
450 assert!(blob.stored, "high-entropy input should fall back to stored");
451 assert_eq!(blob.payload.len(), data.len(), "stored payload must not grow");
452 assert_eq!(blob.ratio(), 1.0);
453 assert_eq!(FractalCompressor::decompress_bytes(&blob).unwrap(), data);
454 }
455
456 #[test]
458 fn test_corrupted_payload_is_rejected() {
459 let compressor = FractalCompressor::new();
460 let data = alloc::vec![b'B'; 2048];
461 let mut blob = compressor.compress_bytes(&data).unwrap();
462 blob.original_hash[0] ^= 0xFF;
463 assert!(FractalCompressor::decompress_bytes(&blob).is_err());
464 }
465
466 #[test]
467 fn test_compress_and_verify() {
468 let compressor = FractalCompressor::new();
469 let data: Vec<u8> = (0..256).map(|i| i as u8).collect();
470 let (blob, decompressed) = compressor.compress_and_verify(&data).unwrap();
471 assert_eq!(decompressed, data);
472 assert_eq!(blob.original_size, 256);
473 }
474
475 #[test]
476 fn test_empty_input_is_an_error() {
477 let compressor = FractalCompressor::new();
478 assert!(compressor.compress_bytes(&[]).is_err());
479 }
480
481 #[test]
482 fn test_reduce_5d_to_3d() {
483 let coords = vec![
484 FiveDimCoord { spatial: 1.0, temporal: 2.0, probabilistic: 3.0, quantum: 4.0, chaotic: 5.0 },
485 FiveDimCoord { spatial: 2.0, temporal: 3.0, probabilistic: 4.0, quantum: 5.0, chaotic: 6.0 },
486 FiveDimCoord { spatial: 3.0, temporal: 4.0, probabilistic: 5.0, quantum: 6.0, chaotic: 7.0 },
487 ];
488 let reduced = reduce_5d_to_3d(&coords);
489 assert_eq!(reduced.len(), 3);
490 for pt in &reduced {
491 assert!(pt[0].is_finite());
492 assert!(pt[1].is_finite());
493 assert!(pt[2].is_finite());
494 }
495 }
496
497 #[test]
498 fn test_reduce_5d_to_3d_empty() {
499 let reduced = reduce_5d_to_3d(&[]);
500 assert!(reduced.is_empty());
501 }
502}