vsf/lib.rs
1// Allow deprecated items within the crate - VSF must handle legacy types internally. External users will still see deprecation warnings when they use legacy APIs.
2#![allow(deprecated)]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5//! # VSF (Versatile Storage Format)
6//!
7//! Self-describing binary format with hierarchical structure, strong typing, and cryptographic primitives.
8//!
9//! ## Features
10//!
11//! - **Self-describing**: Type markers embedded in the data stream
12//! - **Hierarchical**: Offset-based seeking, unlimited nesting depth
13//! - **Strongly-typed**: Primitives (u0-u7, i3-i7, f32, f64, complex), tensors, Spirix Scalars and Circles
14//! - **Cryptographic**: Built-in BLAKE3 hashing and Ed25519 signing
15//! - **Eagle Time**: universal timestamps
16//! - **Huffman text compression**: ~2× compression over UTF-8 for strings
17//!
18//! ## Core Type System
19//!
20//! ### Primitives
21//! - **Integers**: `u0`-`u7` (unsigned), `i3`-`i7` (signed)
22//! - **IEEE Floats**: `f5` (f32), `f6` (f64)
23//! - **IEEE Complex**: `j5` (Complex<f32>), `j6` (Complex<f64>)
24//! - **Spirix**: `s33`-`s77` (Scalar), `c33`-`c77` (Circle)
25//!
26//! ### Tensors
27//! - **Contiguous** (`t`): Row-major multi-dimensional arrays (1D-4D)
28//! - **Strided** (`q`): Non-contiguous views with explicit stride
29//!
30//! ### Metadata and Labels
31//!
32//! VSF uses labels within sections for metadata:
33//!
34//! - `l`: Label text - identifies a field within a section (e.g., "shutter_speed", "author")
35//! - Section fields can contain multiple values: `(label:value1,value2,value3)`
36//! - Sections can contain hierarchical fields: `[dImaging (lshutter_speed:f6{0.01})(laperture:f5{2.8})]`
37//!
38//! **Other Metadata Types:**
39//! - `x`: Huffman compressed Unicode text strings
40//! - `e`: Eagle Time (seconds since lunar landing)
41//! - `d`: Data type identifier
42//! - `o`: Byte offsets
43//! - `b`: Byte lengths
44//! - `n`: Counts
45//! - `g`: Cryptographic signatures
46//! - `h`: Cryptographic hashes
47//!
48//! ## File Structure
49//!
50//! VSF files follow a hierarchical structure:
51//!
52//! ```text
53//! RÅ< Magic number + header start
54//! z3{5} Format version (FIRST - determines encoding)
55//! y3{5} Backward compatibility version
56//! b#{header length} Header size (now we know how to encode it!)
57//! L#{file length} Total file size in bytes (optional, for TCP streaming without parse-as-you-go)
58//! eu6{current time as u64} Eagle Time when emitted (u64 oscillations, 704ps precision). OPTIONAL — devices without a clock (no RTC, no network, pre-handshake) omit the `e` field entirely rather than lie with a placeholder. Readers detect absence by the next byte being `h` (provenance hash) instead of `e`.
59//! hp3{31}{provenance hash} Provenance: BLAKE3 hash of content (required, always 32 bytes)
60//! ge{64}{signature} Ed25519 signature over entire file AFTER provinence hash is patched in (optional, rolling or provinence, must have one or the other)
61//! hb{31}{rolling_hash} Rolling: BLAKE3 of current state with History (optional)
62//! k#{key} File-level encryption key (optional)
63//! n#{field count} Number of fields
64//! (d3{9}raw_image:h#{hash},o#{offset},b#{size},n#{count}) Field with values
65//! (d3{9}thumbnail:h#{hash},o#{offset},b#{size},n#{count}) ...
66//! > Header end
67//!
68//! [(section_fields...)...] Section data at offset for RAW image, note that if section is not encrypted and closer than 1MB from the header, section name, count and length are not required. otherwise all three. [d3{9}thumbnailn#{number of fields}b#{length of section}(section_fields...)...]
69//! ```
70//!
71//! **Hash Strategy (Always BLAKE3):**
72//! - **hp** (hash provenance): Content identity - BLAKE3 hash of immutable content. Required. Computed with hp field as zeros, then filled in. Creates stable identifier for original content.
73//! - **ge** (signature): Optional Ed25519 signature. When signing, compute hp, sign it, then **replace** hp bytes with ge signature.
74//! - **hb** (hash rolling): Current file state - Optional BLAKE3 hash including History section. Updates when History updates. Useful for tracking mutable file evolution. ge or hb, must have one.
75//!
76//! **Provenance Verification:** To verify a file's provenance, zero the hp and signature/rolling hash fields and compute BLAKE3 - it will match the stored hp if original. If present, verify the ge signature against hp to authenticate the creator.
77//!
78//! **Terminology:**
79//! - **Header**: Everything between `RÅ<` and `>`
80//! - **Provenance primitives**: Version, timestamp, hash, signature (NOT wrapped in `()`)
81//! - **Header field**: Section pointer `(d"name" o b n)` with POSITIONAL values (no `:` or `,`)
82//! - **Section**: Actual data blocks after the header, located at specified offsets
83//! - **Section field**: Individual `(field:value)` or `(field:v0,v1)` entries within a section
84//! - **`?` and `{}`**: `?` indicates length (ASCII 0-Z), `{}` indicates binary data
85//!
86//! The `:` and `,` separators in label records make the format human-readable in hex editors and aid in forensics and corruption analysis with minimal overhead.
87//!
88//! ## Section Flattening Example
89//!
90//! A section with hierarchical fields for camera metadata:
91//!
92//! ```text
93//! [d{Imaging} (l{shutter_speed}:f6{0.01}) // 1/100s as f64 (l{aperture}:f5{2.8}) // f/2.8 as f32 (l{iso}:u4{400}) // ISO 400 ]
94//! ```
95//!
96//! Which flattens to:
97//!
98//! ```text
99//! '[' + 'd' + '3' + {7u8} + "Imaging" + '(' + 'l' + '3' + {13u8} + "shutter_speed" + ':' + 'f' + '6' + {0.01f64} + ')' + '(' + 'l' + '3' + {8u8} + "aperture" + ':' + 'f' + '5' + {2.8f32} + ')' + '(' + 'l' + '3' + {3u8} + "iso" + ':' + 'u' + '4'+ {400u16} + ')' + ']'
100//! ```
101//!
102//! Where 'char' indicates a single byte character, and "string" indicates ASCII text bytes.
103//!
104//! And the final flattened byte stream is:
105//!
106//! ```text
107//! [d3{0x07}Imaging(l3{0x0D}shutter_speed:f6{0x7B 14 AE 47 E1 7A 84 3F})(l3{0x08}aperture:f5{0x33 33 33 40})(l3{0x03}iso:u4{0x01 90})]
108//! ```
109//!
110//! Each section field is enclosed by `()`'s and always starts with a text identifier (`l` marker + ASCII string), followed by `:` and its value(s) separated by `,`. Section fields are flattened sequentially, creating a self-describing stream.
111//!
112//! ## Optional History Section (Will change heavily as design matures)
113//!
114//! For applications requiring detailed tracking beyond the immutable creation timestamp:
115//!
116//! ```text
117//! [dHistory (ef6{1234567890.5},hb{256}{hash_at_creation},ltool:x{Lumis},lversion:z{0.1.2},lhost:x{workstation-sea}) (ef6{1234567920.3},hb{256}{hash_after_modify},ltool:x{Photon},laction:x{modified},lhost:x{laptop-pdx}) (ef6{1234567950.1},hb{256}{hash_after_access},laction:x{accessed},lhost:x{phone-mobile}) ]
118//! ```
119//!
120//! Each history entry records the file's `hb` hash at that point in time, creating a verifiable chain of file states. To verify history integrity, recompute `hb` for each historical state by truncating the History section to that entry.
121//!
122//! Which flattens to:
123//!
124//! ```text
125//! '[' + 'd' + '1' + {7u8} + "History" + '(' + 'e' + 'f' + '6' + {1234567890.5f64} + ',' + 'h' + 'b' + '3' + {32u8} + {32 bytes BLAKE3 hash} + ',' + 'l' + '1' + {4u8} + "tool" + ':' + 'x' + '1' + {5u8} + "Lumis" + ',' + 'l' + '1' + {7u8} + "version" + ':' + 'z' + '1' + {5u8} + "0.1.2" + ',' + 'l' + '1' + {4u8} + "host" + ':' + 'x' + '2' + {15u8} + "workstation-sea" + ')' + '(' + 'e' + 'f' + '6' + {1234567920.3f64} + ',' + 'h' + 'b' + '3' + {32u8} + {32 bytes BLAKE3 hash} + ',' + 'l' + '1' + {4u8} + "tool" + ':' + 'x' + '1' + {6u8} + "Photon" + ',' + 'l' + '1' + {6u8} + "action" + ':' + 'x' + '1' + {8u8} + "modified" + ',' + 'l' + '1' + {4u8} + "host" + ':' + 'x' + '1' + {10u8} + "laptop-pdx" + ')' + '(' + 'e' + 'f' + '6' + {1234567950.1f64} + ',' + 'h' + 'b' + '3' + {32u8} + {32 bytes BLAKE3 hash} + ',' + 'l' + '1' + {6u8} + "action" + ':' + 'x' + '1' + {8u8} + "accessed" + ',' + 'l' + '1' + {4u8} + "host" + ':' + 'x' + '1' + {12u8} + "mobile" + ')' + ']'
126//! ```
127//!
128//! Each history entry is a complete event enclosed in `()`'s with timestamp, tool, action, and context. The History section has its own hash in the header label record for integrity verification, but is NOT included in `hs` (static content hash). It IS included in `hb` (rolling file hash).
129//!
130//! ## Quick Start
131//!
132//! ```
133//! use vsf::{VsfType, VsfBuilder, Tensor, parse};
134//!
135//! // Encode a tensor
136//! let tensor = Tensor::new(vec![3, 4], vec![1u16, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
137//! let encoded = VsfType::t_u4(tensor).flatten();
138//!
139//! // Decode it back
140//! let mut ptr = 0;
141//! let decoded = parse(&encoded, &mut ptr).unwrap();
142//!
143//! // Build a complete VSF file with header
144//! let vsf_file = VsfBuilder::new()
145//! .add_section("metadata", vec![
146//! ("width".to_string(), VsfType::u(1920, false)),
147//! ("height".to_string(), VsfType::u(1080, false)),
148//! ])
149//! .add_unboxed("pixels", vec![0xFF; 1024])
150//! .build()
151//! .unwrap();
152//! ```
153//!
154//! ## Eagle Time Formats
155//!
156//! Eagle Time counts oscillations since 1969-07-20 20:17:40 UTC (Apollo 11 lunar landing). Always coordinated, no timezones, no daylight saving. One universal time standard.
157//!
158//! - **eu6**: 64-bit oscillation count (`u64`) - 704ps precision, deterministic integer timestamps (default)
159//! - **ef5**: 32-bit float (`f32`) - ~2 minute precision, legacy compact format
160//! - **ef6**: 64-bit float (`f64`) - ~200ns precision, legacy high-accuracy format
161//!
162//! The format version doesn't change the epoch or oscillation frequency - 1,420,407,826 Hz (21cm hydrogen line).
163//!
164//! ### Optional in the header
165//!
166//! Eagle Time is OPTIONAL in the file header. Devices that genuinely cannot know what time it is — embedded sensors without an RTC, freshly-booted SoCs before any network sync, single-purpose ASICs — omit the `e` field rather than emit a placeholder. A reader that finds the next byte after `l` (file length) is `h` (provenance hash) rather than `e` knows the producer was clockless and skips the field. Devices that DO know the time (host CLIs via `nunc-time`, kernels via QTIMER, anything network-synced) include it normally. Lying about the time is worse than admitting you don't know it.
167//!
168//! ## Parsing and Encoding
169//!
170//! **Element-level parsing:**
171//! ```ignore
172//! use vsf::parse; let data = vec![b'u', b'3', 42]; let mut ptr = 0; let value = parse(&data, &mut ptr)?; // Parses one VsfType element
173//! ```
174//!
175//! **Header encoding (VsfHeader):**
176//! ```ignore
177//! use vsf::file_format::VsfHeader; let mut header = VsfHeader::new(version, backward_compat); header.add_field(field); let bytes = header.encode()?; // Encodes header to bytes
178//! ```
179//!
180//! **Header decoding (VsfHeader):**
181//! ```ignore
182//! use vsf::file_format::VsfHeader; let (header, header_end) = VsfHeader::decode(&bytes)?; // Parses the full RÅ< header
183//! ```
184//! `VsfHeader::decode()` is implemented at [file_format.rs](src/file_format.rs) (`VsfHeader::decode`); it returns the parsed header plus the byte offset where the header ends.
185//!
186//! ## Parsing APIs: Two Tiers
187//!
188//! VSF provides two parsing approaches for sections, each suited to different use cases:
189//!
190//! ### Low-Level: `VsfSection::parse()` ([file_format.rs](src/file_format.rs))
191//!
192//! Schema-agnostic parsing that extracts raw data without validation:
193//!
194//!
195//! ```ignore
196//! use vsf::VsfSection;
197//!
198//! let mut ptr = 0; let section = VsfSection::parse(&bytes, &mut ptr)?; // Returns VsfSection with name and Vec<VsfField> // No schema required, no validation performed
199//! ```
200//!
201//! **Use when:**
202//! - Reading unknown/arbitrary VSF data
203//! - Debugging or inspecting files
204//! - Building tooling that handles any section type
205//! - You don't have or need a schema
206//!
207//! ### High-Level: `SectionBuilder::parse()` ([schema/section.rs](src/schema/section.rs))
208//!
209//! Schema-validated parsing for type-safe workflows:
210//!
211//!
212//! ```ignore
213//! use vsf::schema::{SectionSchema, SectionBuilder, TypeConstraint};
214//!
215//! let schema = SectionSchema::new("camera") .field("iso", TypeConstraint::AnyUnsigned) .field("shutter", TypeConstraint::AnyFloat);
216//!
217//! let builder = SectionBuilder::parse(schema, §ion_bytes)?; // Validates section name matches schema // Validates each field against type constraints // Returns SectionBuilder for modify → re-encode workflow
218//! ```
219//!
220//! **Use when:**
221//! - You know the expected structure
222//! - Type safety and validation matter
223//! - You need to modify and re-encode sections
224//! - Building applications with defined schemas
225//!
226//! Both parse the same `[d"name"(d"field":value)...]` binary format—`SectionBuilder` adds schema enforcement on top of the low-level parsing.
227//!
228//! ## Module Structure
229//!
230//! - `types` - Core type definitions (VsfType, Tensor, EagleTime, WorldCoord)
231//! - `encoding` - Binary serialization (exponential-width integers, flatten)
232//! - `decoding` - Binary parsing with `parse()` function
233//! - `file_format` - VSF file headers and sections (VsfHeader, VsfSection)
234//! - `vsf_builder` - High-level builder for complete files
235//! - `schema` - Type-safe section schemas with field validation and parse→modify→encode
236//! - `verification` - Cryptographic hashing and signing
237//! - `crypto_algorithms` - Algorithm identifiers for hashes, signatures, keys, MACs
238//! - `decrypt` - Decryption utilities (requires `crypto` feature)
239//! - `text_encoding` - Huffman compression for Unicode strings (requires `text` feature)
240//! - `colour` - Colourspace conversions (VSF RGB, Rec.2020, sRGB, XYZ)
241//! - `builders` - Domain-specific builders (RAW images)
242//! - `inspect` - Inspection and formatting utilities (requires `inspect` feature)
243//!
244
245extern crate alloc;
246
247/// Build an error string for an unexpected VsfType variant at decode time.
248///
249/// With the `errors-verbose` feature (default-on), expands to `format!` of the expected-label plus a `{:?}` of the value. Without it, expands to a static string with just the expected label and drops the value via `let _ = &v;`.
250///
251/// Disabling `errors-verbose` lets the linker remove `<VsfType as Debug>::fmt` — which transitively removes the IEEE-754 grisu/dragon float formatters (~10 KB on cortex-m) since they're only kept alive by the float variants' Debug arms.
252#[macro_export]
253macro_rules! type_mismatch_err {
254 ($expected:literal, $got:expr) => {{
255 #[cfg(feature = "errors-verbose")]
256 {
257 ::alloc::format!(concat!($expected, ", got {:?}"), $got)
258 }
259 #[cfg(not(feature = "errors-verbose"))]
260 {
261 let _ = &$got;
262 <::alloc::string::String as ::core::convert::From<&str>>::from($expected)
263 }
264 }};
265}
266
267/// no_std-friendly prelude: re-exports of the `alloc` types that std's prelude provides automatically. Individual files import via `use crate::prelude::*;` to stay compatible across `std` and `no_std + alloc` builds.
268pub mod prelude {
269 pub use alloc::borrow::ToOwned;
270 pub use alloc::boxed::Box;
271 pub use alloc::format;
272 pub use alloc::string::{String, ToString};
273 pub use alloc::vec;
274 pub use alloc::vec::Vec;
275}
276
277// VSF format version constants
278/// Current VSF format version v9: spirix payloads (rd/rb/rw/rq and everything nesting them) reinterpreted under spirix 0.1 semantics: implicit-sign Scalar fraction, AMBIG=0 exponent convention, Circle keeps the N1 fraction. Same bytes decode to DIFFERENT values than v8, so this is a wire break for spirix-carrying files even tho non-spirix files are bit-identical. v8: x text codebook remapped to cover the full Unicode codespace (all 1,112,064 codepoints pre-assigned) + NFC canonicalization at the encoder boundary. Changed the Huffman bitstream for every x value — even pure-ASCII strings — so v7 x bytes and v8 x bytes are mutually unintelligible. v7: Added opcodes (op type), literal VSF format, proper bracket notation (⦉⦊ vs {})
279pub const VSF_VERSION: usize = 9;
280
281/// Oldest format version this BUILD can read — the floor is build-time chosen, cargo-style. Default is the current version only: wire breaks are never silently bridged, and a too-old file is rejected by name at the header. The plan for honoring old archives: a `compat-v*` feature per legacy dialect lowers this floor AND compiles in that era's decode paths, dispatched on the file's z at parse time; when none is enabled, old files get the loud version error (never a wrong parse). No compat feature may ship as a floor-only no-op — lowering the floor without the era's real readers recreates silent misinterpretation. None exist yet: a true `compat-v8` needs spirix 0.0.x payload decode vendored (the old Scalar bit semantics), and a true `compat-v7` needs the pre-remap x codebook vendored from git (c12c7e2~1) plus the l-as-ASCII-label mapping, and v7 sub-dialects (l→a marker remap ~0.3.5, x codebook remap 0.4.0) shipped without z bumps, so "v7" in the field is ambiguous anyway — per-archive forensics if real v7 data ever needs recovering.
282pub const VSF_BACKWARD_COMPAT: usize = 9;
283
284// THE GATE: the crate's semver-breaking number IS the format version, enforced at compile time.
285// While vsf is 0.x the breaking number is the MINOR (0.9.z ⇒ format v9); at 1.0+ it becomes the MAJOR.
286// cargo publish runs a verify build, so a stale VSF_VERSION cannot reach crates.io — it fails right here first.
287// Four VSF releases (plus one spirix) have been yanked because a human forgot this bump; no human step remains.
288const fn semver_breaking_number(version: &str) -> usize {
289 let b = version.as_bytes();
290 let mut i = 0;
291 let mut major = 0;
292 while i < b.len() && b[i] != b'.' {
293 major = major * 10 + (b[i] - b'0') as usize;
294 i += 1;
295 }
296 if major > 0 {
297 return major;
298 }
299 i += 1;
300 let mut minor = 0;
301 while i < b.len() && b[i] != b'.' {
302 minor = minor * 10 + (b[i] - b'0') as usize;
303 i += 1;
304 }
305 minor
306}
307const _: () = assert!(
308 VSF_VERSION == semver_breaking_number(env!("CARGO_PKG_VERSION")),
309 "VSF_VERSION does not match the semver-breaking number in Cargo.toml. Bump VSF_VERSION (and review VSF_BACKWARD_COMPAT with it) — the z marker is the wire contract, not a formality."
310);
311const _: () = assert!(
312 VSF_BACKWARD_COMPAT <= VSF_VERSION,
313 "VSF_BACKWARD_COMPAT cannot exceed VSF_VERSION — the compat floor can only reach backward."
314);
315
316// Core type system
317pub mod types;
318
319// Binary encoding
320pub mod encoding;
321
322// Binary decoding
323pub mod decoding;
324
325// High-level builders for common use cases
326pub mod builders;
327
328// Huffman text encoding for `x` marker
329#[cfg(any(feature = "text", feature = "text-encode"))]
330pub mod text_encoding;
331
332// VSF file format with headers and labels
333pub mod file_format;
334
335// VSF file builder
336pub mod vsf_builder;
337
338// Schema system for type-safe sections (experimental)
339pub mod schema;
340
341// Cryptographic algorithm identifiers (h, g, k types)
342pub mod crypto_algorithms;
343
344// Verification functions for hashing and signing VSF files
345pub mod verification;
346
347// Handle identity: plaintext → proof-of-work → public ID (requires ihi, gated to break cycle: ihi depends on vsf for text-encode)
348#[cfg(feature = "handle")]
349pub mod handle;
350
351// Decryption utilities
352#[cfg(feature = "crypto")]
353pub mod decrypt;
354
355// Colour system (spectral and legacy colourspaces)
356pub mod colour;
357
358// Theme definitions for inspection output
359pub mod themes;
360
361// Inspection and formatting utilities (coloured output)
362#[cfg(feature = "inspect")]
363pub mod inspect;
364
365// VSF image decode (tensor + rav1d AV1) → α + darkness buffer. Counterpart to the `vsfimg` encoder; feeds fluor's `Icon` and toka's `Canvas` (shared α+darkness convention). Pulls rav1d, so it's opt-in — off for size-critical consumers (the fgtw-bootstrap worker).
366#[cfg(feature = "image-decode")]
367pub mod image;
368
369// VSF-Image v0 — spectral-first raw image container (K channels of sensor counts + per-channel spectral response + ihi provenance ingredients). Pure VSF types, no heavy deps, so it's unconditional. Consumed by opsin (viewer/converter) and chameleon.
370pub mod spectral_image;
371
372// Re-export main types
373pub use types::{
374 datetime_to_eagle_time, BitPackedTensor, EagleTime, EtType, LayoutOrder, NaScheme,
375 StridedTensor, Tensor, VsfType, WaAddress, WorldCoord, OSCILLATIONS_PER_SECOND,
376};
377#[cfg(feature = "std")]
378pub use types::{eagle_time_nanos, eagle_time_oscillations};
379
380// Re-export colour conversion types
381pub use colour::convert::{ColourFormat, RgbLinearF32, RgbaLinearF32};
382
383// Re-export encoding traits
384pub use encoding::{EncodeNumber, EncodeNumberInclusive};
385
386// Re-export decoding function
387pub use decoding::parse;
388
389// Re-export file format and builder
390pub use file_format::{validate_name, HeaderField, VsfField, VsfHeader, VsfSection};
391pub use vsf_builder::{SectionMeta, VsfBuilder};
392
393// RAW image builders and parser
394pub use builders::{
395 build_raw_image,
396 lumis_raw_capture,
397 parse_raw_image,
398 // Newtype wrappers for type safety
399 Aperture,
400 BlackLevel,
401 CalibrationHash,
402 // Builder structs
403 CameraBuilder,
404 CameraSettings,
405 CfaPattern,
406 ExposureCompensation,
407 FlashFired,
408 FocalLength,
409 FocusDistance,
410 IsoSpeed,
411 LensBuilder,
412 LensInfo,
413 Magic9,
414 Manufacturer,
415 MeteringMode,
416 ModelName,
417 ParsedRawImage,
418 RawImageBuilder,
419 RawMetadata,
420 RawMetadataBuilder,
421 SerialNumber,
422 ShutterTime,
423 WhiteLevel,
424};
425
426// Coming soon pub mod registry; // Metadata key registry
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_tensor_creation() {
434 let tensor = Tensor::new(vec![3, 4], vec![0u8; 12]);
435 assert_eq!(tensor.len(), 12);
436 assert_eq!(tensor.ndim(), 2);
437 assert_eq!(tensor.shape, vec![3, 4]);
438 assert!(!tensor.is_empty());
439 }
440
441 #[test]
442 fn test_strided_tensor_contiguous() {
443 // Row-major (contiguous)
444 let row_major = StridedTensor::new(vec![3, 4], vec![4, 1], vec![0u8; 12]);
445 assert!(row_major.is_contiguous());
446
447 // Column-major (non-contiguous in row-major memory)
448 let col_major = StridedTensor::new(vec![3, 4], vec![1, 3], vec![0u8; 12]);
449 assert!(!col_major.is_contiguous());
450 }
451
452 #[test]
453 fn test_tensor_dimensions() {
454 let t1d = Tensor::new(vec![100], vec![0u32; 100]);
455 assert_eq!(t1d.ndim(), 1);
456
457 let t2d = Tensor::new(vec![10, 20], vec![0u16; 200]);
458 assert_eq!(t2d.ndim(), 2);
459
460 let t3d = Tensor::new(vec![5, 10, 20], vec![0u8; 1000]);
461 assert_eq!(t3d.ndim(), 3);
462
463 let t4d = Tensor::new(vec![2, 3, 4, 5], vec![0f32; 120]);
464 assert_eq!(t4d.ndim(), 4);
465 }
466
467 #[test]
468 #[should_panic(expected = "Data length 10 doesn't match shape")]
469 fn test_tensor_size_validation() {
470 // This should panic: 3×4 = 12, but we only gave 10 elements
471 Tensor::new(vec![3, 4], vec![0u8; 10]);
472 }
473
474 // ==================== ROUND-TRIP TESTS ====================
475
476 #[test]
477 fn test_roundtrip_unsigned() {
478 // u0 (bool)
479 let val = VsfType::u0(true);
480 let flat = val.flatten();
481 let mut ptr = 0;
482 let parsed = parse(&flat, &mut ptr).unwrap();
483 assert_eq!(ptr, flat.len());
484 if let VsfType::u0(v) = parsed {
485 assert_eq!(v, true);
486 } else {
487 panic!("Expected u0");
488 }
489
490 // u3
491 let val = VsfType::u3(42);
492 let flat = val.flatten();
493 let mut ptr = 0;
494 let parsed = parse(&flat, &mut ptr).unwrap();
495 assert_eq!(ptr, flat.len());
496 if let VsfType::u3(v) = parsed {
497 assert_eq!(v, 42);
498 } else {
499 panic!("Expected u3");
500 }
501
502 // u4
503 let val = VsfType::u4(1000);
504 let flat = val.flatten();
505 let mut ptr = 0;
506 let parsed = parse(&flat, &mut ptr).unwrap();
507 if let VsfType::u4(v) = parsed {
508 assert_eq!(v, 1000);
509 } else {
510 panic!("Expected u4");
511 }
512
513 // u5
514 let val = VsfType::u5(100000);
515 let flat = val.flatten();
516 let mut ptr = 0;
517 let parsed = parse(&flat, &mut ptr).unwrap();
518 if let VsfType::u5(v) = parsed {
519 assert_eq!(v, 100000);
520 } else {
521 panic!("Expected u5");
522 }
523 }
524
525 #[test]
526 fn test_roundtrip_signed() {
527 // i3
528 let val = VsfType::i3(-42);
529 let flat = val.flatten();
530 let mut ptr = 0;
531 let parsed = parse(&flat, &mut ptr).unwrap();
532 if let VsfType::i3(v) = parsed {
533 assert_eq!(v, -42);
534 } else {
535 panic!("Expected i3");
536 }
537
538 // i5
539 let val = VsfType::i5(-100000);
540 let flat = val.flatten();
541 let mut ptr = 0;
542 let parsed = parse(&flat, &mut ptr).unwrap();
543 if let VsfType::i5(v) = parsed {
544 assert_eq!(v, -100000);
545 } else {
546 panic!("Expected i5");
547 }
548 }
549
550 #[test]
551 fn test_roundtrip_float() {
552 // f5
553 let val = VsfType::f5(3.14159);
554 let flat = val.flatten();
555 let mut ptr = 0;
556 let parsed = parse(&flat, &mut ptr).unwrap();
557 if let VsfType::f5(v) = parsed {
558 assert!((v - 3.14159).abs() < 0.00001);
559 } else {
560 panic!("Expected f5");
561 }
562
563 // f6
564 let val = VsfType::f6(2.718281828459045);
565 let flat = val.flatten();
566 let mut ptr = 0;
567 let parsed = parse(&flat, &mut ptr).unwrap();
568 if let VsfType::f6(v) = parsed {
569 assert!((v - 2.718281828459045).abs() < 1e-15);
570 } else {
571 panic!("Expected f6");
572 }
573 }
574
575 #[test]
576 fn test_roundtrip_complex() {
577 use num_complex::Complex;
578
579 // j5
580 let val = VsfType::j5(Complex::new(1.0f32, 2.0f32));
581 let flat = val.flatten();
582 let mut ptr = 0;
583 let parsed = parse(&flat, &mut ptr).unwrap();
584 if let VsfType::j5(v) = parsed {
585 assert!((v.re - 1.0).abs() < 0.00001);
586 assert!((v.im - 2.0).abs() < 0.00001);
587 } else {
588 panic!("Expected j5");
589 }
590
591 // j6
592 let val = VsfType::j6(Complex::new(3.14, -2.71));
593 let flat = val.flatten();
594 let mut ptr = 0;
595 let parsed = parse(&flat, &mut ptr).unwrap();
596 if let VsfType::j6(v) = parsed {
597 assert!((v.re - 3.14).abs() < 1e-15);
598 assert!((v.im + 2.71).abs() < 1e-15);
599 } else {
600 panic!("Expected j6");
601 }
602 }
603
604 #[cfg(feature = "text-encode")]
605 #[test]
606 fn test_roundtrip_string() {
607 let val = VsfType::x("Hello, VSF!".to_string());
608 let flat = val.flatten();
609 let mut ptr = 0;
610 let parsed = parse(&flat, &mut ptr).unwrap();
611 if let VsfType::x(v) = parsed {
612 assert_eq!(v, "Hello, VSF!");
613 } else {
614 panic!("Expected x");
615 }
616 }
617
618 #[test]
619 #[allow(non_snake_case)]
620 fn test_roundtrip_hP() {
621 let bytes = vec![0xABu8; 32];
622 let val = VsfType::hP(bytes.clone());
623 let flat = val.flatten();
624 let mut ptr = 0;
625 let parsed = parse(&flat, &mut ptr).unwrap();
626 assert!(
627 matches!(parsed, VsfType::hP(ref b) if *b == bytes),
628 "hP round-trip failed"
629 );
630 }
631
632 #[test]
633 #[allow(non_snake_case)]
634 fn test_roundtrip_hR() {
635 let bytes = vec![0xCDu8; 32];
636 let val = VsfType::hR(bytes.clone());
637 let flat = val.flatten();
638 let mut ptr = 0;
639 let parsed = parse(&flat, &mut ptr).unwrap();
640 assert!(
641 matches!(parsed, VsfType::hR(ref b) if *b == bytes),
642 "hR round-trip failed"
643 );
644 }
645
646 #[test]
647 #[allow(non_snake_case)]
648 fn test_roundtrip_hI() {
649 let bytes = vec![0x11u8; 32];
650 let val = VsfType::hI(bytes.clone());
651 let flat = val.flatten();
652 let mut ptr = 0;
653 let parsed = parse(&flat, &mut ptr).unwrap();
654 assert!(
655 matches!(parsed, VsfType::hI(ref b) if *b == bytes),
656 "hI round-trip failed"
657 );
658 }
659
660 #[test]
661 #[allow(non_snake_case)]
662 fn test_roundtrip_hV() {
663 let bytes = vec![0x22u8; 32];
664 let val = VsfType::hV(bytes.clone());
665 let flat = val.flatten();
666 let mut ptr = 0;
667 let parsed = parse(&flat, &mut ptr).unwrap();
668 assert!(
669 matches!(parsed, VsfType::hV(ref b) if *b == bytes),
670 "hV round-trip failed"
671 );
672 }
673
674 #[test]
675 fn test_roundtrip_metadata() {
676 // ASCII text
677 let val = VsfType::a("test_label".to_string());
678 let flat = val.flatten();
679 let mut ptr = 0;
680 let parsed = parse(&flat, &mut ptr).unwrap();
681 if let VsfType::a(v) = parsed {
682 assert_eq!(v, "test_label");
683 } else {
684 panic!("Expected a");
685 }
686
687 // Offset
688 let val = VsfType::o(1024);
689 let flat = val.flatten();
690 let mut ptr = 0;
691 let parsed = parse(&flat, &mut ptr).unwrap();
692 if let VsfType::o(v) = parsed {
693 assert_eq!(v, 1024);
694 } else {
695 panic!("Expected o");
696 }
697
698 // Version
699 let val = VsfType::z(42);
700 let flat = val.flatten();
701 let mut ptr = 0;
702 let parsed = parse(&flat, &mut ptr).unwrap();
703 if let VsfType::z(v) = parsed {
704 assert_eq!(v, 42);
705 } else {
706 panic!("Expected z");
707 }
708 }
709
710 #[test]
711 fn test_roundtrip_eagle_time() {
712 // e6: canonical 64-bit oscillation count
713 let val = VsfType::e(EtType::e6(1000000));
714 let flat = val.flatten();
715 assert_eq!(flat[0], b'e');
716 assert_eq!(flat[1], b'6');
717 assert_eq!(flat.len(), 10); // 2 tag + 8 bytes
718 let mut ptr = 0;
719 let parsed = parse(&flat, &mut ptr).unwrap();
720 if let VsfType::e(EtType::e6(v)) = parsed {
721 assert_eq!(v, 1000000);
722 } else {
723 panic!("Expected e(e6)");
724 }
725
726 // e5: 32-bit oscillation count
727 let val = VsfType::e(EtType::e5(42000));
728 let flat = val.flatten();
729 assert_eq!(flat[0], b'e');
730 assert_eq!(flat[1], b'5');
731 assert_eq!(flat.len(), 6); // 2 tag + 4 bytes
732 let mut ptr = 0;
733 let parsed = parse(&flat, &mut ptr).unwrap();
734 if let VsfType::e(EtType::e5(v)) = parsed {
735 assert_eq!(v, 42000);
736 } else {
737 panic!("Expected e(e5)");
738 }
739
740 // e7: 128-bit oscillation count
741 let val = VsfType::e(EtType::e7(999_999_999_999_999_999_i128));
742 let flat = val.flatten();
743 assert_eq!(flat[0], b'e');
744 assert_eq!(flat[1], b'7');
745 assert_eq!(flat.len(), 18); // 2 tag + 16 bytes
746 let mut ptr = 0;
747 let parsed = parse(&flat, &mut ptr).unwrap();
748 if let VsfType::e(EtType::e7(v)) = parsed {
749 assert_eq!(v, 999_999_999_999_999_999_i128);
750 } else {
751 panic!("Expected e(e7)");
752 }
753
754 // deprecated ef6: still round-trips
755 #[allow(deprecated)]
756 {
757 let val = VsfType::e(EtType::f6(123456.789));
758 let flat = val.flatten();
759 let mut ptr = 0;
760 let parsed = parse(&flat, &mut ptr).unwrap();
761 if let VsfType::e(EtType::f6(v)) = parsed {
762 assert!((v - 123456.789).abs() < 1e-10);
763 } else {
764 panic!("Expected e(f6)");
765 }
766 }
767 }
768
769 #[test]
770 fn test_roundtrip_tensor_small() {
771 // 2D tensor of u16 (3x4)
772 let data = vec![1u16, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
773 let tensor = Tensor::new(vec![3, 4], data.clone());
774 let val = VsfType::t_u4(tensor);
775 let flat = val.flatten();
776
777 let mut ptr = 0;
778 let parsed = parse(&flat, &mut ptr).unwrap();
779
780 if let VsfType::t_u4(t) = parsed {
781 assert_eq!(t.shape, vec![3, 4]);
782 assert_eq!(t.data, data);
783 assert_eq!(ptr, flat.len()); // Consumed all bytes
784 } else {
785 panic!("Expected t_u4 tensor");
786 }
787 }
788
789 #[test]
790 fn test_roundtrip_tensor_1d() {
791 // 1D tensor of i32 - encodes as tensor, decodes as vector (1D optimization)
792 let data = vec![-100i32, 0, 100, 200, -50];
793 let tensor = Tensor::new(vec![5], data.clone());
794 let val = VsfType::t_i5(tensor);
795 let flat = val.flatten();
796
797 let mut ptr = 0;
798 let parsed = parse(&flat, &mut ptr).unwrap();
799
800 // 1D tensors decode as vectors (compact representation)
801 if let VsfType::v_i5(v) = parsed {
802 assert_eq!(v.data, data);
803 } else {
804 panic!("Expected v_i5 vector, got {:?}", parsed);
805 }
806 }
807
808 // ==================== 1D VECTOR OPTIMIZATION TESTS ====================
809
810 #[test]
811 fn test_1d_vector_optimization_unsigned() {
812 // Test all unsigned types with 1D vector optimization 1D tensors encode with 'tn' format and decode as vectors Exception: u8 stays as tensor (since Vec<u8> == raw bytes)
813
814 // u8 vector - special case: stays as tensor
815 let data_u8 = vec![1u8, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200];
816 let tensor_u8 = Tensor::new(vec![12], data_u8.clone());
817 let val_u8 = VsfType::t_u3(tensor_u8);
818 let flat_u8 = val_u8.flatten();
819
820 // Check that it uses 'n' format (compact)
821 assert_eq!(flat_u8[0], b't');
822 assert_eq!(flat_u8[1], b'n'); // Should use 'n' for 1D
823
824 let mut ptr = 0;
825 let parsed_u8 = parse(&flat_u8, &mut ptr).unwrap();
826 // u8 is special: decodes back as tensor (raw byte compatibility)
827 if let VsfType::t_u3(t) = parsed_u8 {
828 assert_eq!(t.shape, vec![12]);
829 assert_eq!(t.data, data_u8);
830 assert_eq!(ptr, flat_u8.len());
831 } else {
832 panic!("Expected t_u3 tensor, got {:?}", parsed_u8);
833 }
834
835 // u16 vector
836 let data_u16 = vec![100u16, 200, 300, 400, 500, 1000, 2000, 3000];
837 let tensor_u16 = Tensor::new(vec![8], data_u16.clone());
838 let val_u16 = VsfType::t_u4(tensor_u16);
839 let flat_u16 = val_u16.flatten();
840
841 assert_eq!(flat_u16[0], b't');
842 assert_eq!(flat_u16[1], b'n'); // Should use 'n' for 1D
843
844 let mut ptr = 0;
845 let parsed_u16 = parse(&flat_u16, &mut ptr).unwrap();
846 if let VsfType::v_u4(v) = parsed_u16 {
847 assert_eq!(v.data, data_u16);
848 } else {
849 panic!("Expected v_u4 vector, got {:?}", parsed_u16);
850 }
851
852 // u32 vector
853 let data_u32 = vec![100000u32, 200000, 300000, 400000, 500000];
854 let tensor_u32 = Tensor::new(vec![5], data_u32.clone());
855 let val_u32 = VsfType::t_u5(tensor_u32);
856 let flat_u32 = val_u32.flatten();
857
858 assert_eq!(flat_u32[0], b't');
859 assert_eq!(flat_u32[1], b'n'); // Should use 'n' for 1D
860
861 let mut ptr = 0;
862 let parsed_u32 = parse(&flat_u32, &mut ptr).unwrap();
863 if let VsfType::v_u5(v) = parsed_u32 {
864 assert_eq!(v.data, data_u32);
865 } else {
866 panic!("Expected v_u5 vector, got {:?}", parsed_u32);
867 }
868
869 // u64 vector
870 let data_u64 = vec![1_000_000_000u64, 2_000_000_000, 3_000_000_000];
871 let tensor_u64 = Tensor::new(vec![3], data_u64.clone());
872 let val_u64 = VsfType::t_u6(tensor_u64);
873 let flat_u64 = val_u64.flatten();
874
875 assert_eq!(flat_u64[0], b't');
876 assert_eq!(flat_u64[1], b'n'); // Should use 'n' for 1D
877
878 let mut ptr = 0;
879 let parsed_u64 = parse(&flat_u64, &mut ptr).unwrap();
880 if let VsfType::v_u6(v) = parsed_u64 {
881 assert_eq!(v.data, data_u64);
882 } else {
883 panic!("Expected v_u6 vector, got {:?}", parsed_u64);
884 }
885 }
886
887 #[test]
888 fn test_1d_vector_optimization_signed() {
889 // Test all signed types with 1D vector optimization 1D tensors encode with 'tn' format and decode as vectors
890
891 // i8 vector
892 let data_i8 = vec![-10i8, -5, 0, 5, 10, 20, -20, 30];
893 let tensor_i8 = Tensor::new(vec![8], data_i8.clone());
894 let val_i8 = VsfType::t_i3(tensor_i8);
895 let flat_i8 = val_i8.flatten();
896
897 assert_eq!(flat_i8[0], b't');
898 assert_eq!(flat_i8[1], b'n'); // Should use 'n' for 1D
899
900 let mut ptr = 0;
901 let parsed_i8 = parse(&flat_i8, &mut ptr).unwrap();
902 if let VsfType::v_i3(v) = parsed_i8 {
903 assert_eq!(v.data, data_i8);
904 } else {
905 panic!("Expected v_i3 vector, got {:?}", parsed_i8);
906 }
907
908 // i16 vector
909 let data_i16 = vec![-1000i16, -500, 0, 500, 1000];
910 let tensor_i16 = Tensor::new(vec![5], data_i16.clone());
911 let val_i16 = VsfType::t_i4(tensor_i16);
912 let flat_i16 = val_i16.flatten();
913
914 assert_eq!(flat_i16[0], b't');
915 assert_eq!(flat_i16[1], b'n'); // Should use 'n' for 1D
916
917 let mut ptr = 0;
918 let parsed_i16 = parse(&flat_i16, &mut ptr).unwrap();
919 if let VsfType::v_i4(v) = parsed_i16 {
920 assert_eq!(v.data, data_i16);
921 } else {
922 panic!("Expected v_i4 vector, got {:?}", parsed_i16);
923 }
924
925 // i32 vector
926 let data_i32 = vec![-100000i32, 0, 100000, 200000, -50000];
927 let tensor_i32 = Tensor::new(vec![5], data_i32.clone());
928 let val_i32 = VsfType::t_i5(tensor_i32);
929 let flat_i32 = val_i32.flatten();
930
931 assert_eq!(flat_i32[0], b't');
932 assert_eq!(flat_i32[1], b'n'); // Should use 'n' for 1D
933
934 let mut ptr = 0;
935 let parsed_i32 = parse(&flat_i32, &mut ptr).unwrap();
936 if let VsfType::v_i5(v) = parsed_i32 {
937 assert_eq!(v.data, data_i32);
938 } else {
939 panic!("Expected v_i5 vector, got {:?}", parsed_i32);
940 }
941 }
942
943 #[test]
944 fn test_1d_vector_vs_multi_dim_encoding() {
945 // Verify that 1D uses compact format while multi-dim uses full format
946
947 // 1D vector
948 let data_1d = vec![1u16, 2, 3, 4, 5];
949 let tensor_1d = Tensor::new(vec![5], data_1d.clone());
950 let val_1d = VsfType::t_u4(tensor_1d);
951 let flat_1d = val_1d.flatten();
952
953 // 2D tensor
954 let data_2d = vec![1u16, 2, 3, 4, 5, 6];
955 let tensor_2d = Tensor::new(vec![2, 3], data_2d.clone());
956 let val_2d = VsfType::t_u4(tensor_2d);
957 let flat_2d = val_2d.flatten();
958
959 // 1D should use 'n' format
960 assert_eq!(flat_1d[0], b't');
961 assert_eq!(flat_1d[1], b'n');
962
963 // 2D should use 'u' (ndim) format
964 assert_eq!(flat_2d[0], b't');
965 assert_ne!(flat_2d[1], b'n'); // Should NOT be 'n'
966
967 // 1D should be more compact Format comparison: 1D: t n <count> u 4 <data> 2D: t <ndim> u 4 <shape[0]> <shape[1]> <data> For small shapes, 1D should save bytes
968 assert!(flat_1d.len() <= flat_2d.len());
969 }
970
971 #[test]
972 fn test_1d_vector_large() {
973 // Test with a larger vector (FGTW use case: 32-byte hashes)
974 let hash_data = vec![
975 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB,
976 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67,
977 0x89, 0xAB, 0xCD, 0xEF,
978 ];
979
980 let tensor = Tensor::new(vec![32], hash_data.clone());
981 let val = VsfType::t_u3(tensor);
982 let flat = val.flatten();
983
984 // Verify compact format
985 assert_eq!(flat[0], b't');
986 assert_eq!(flat[1], b'n');
987
988 // Round-trip
989 let mut ptr = 0;
990 let parsed = parse(&flat, &mut ptr).unwrap();
991 if let VsfType::t_u3(t) = parsed {
992 assert_eq!(t.shape, vec![32]);
993 assert_eq!(t.data, hash_data);
994 assert_eq!(ptr, flat.len());
995 } else {
996 panic!("Expected t_u3 tensor");
997 }
998 }
999
1000 #[test]
1001 fn test_roundtrip_tensor_f64() {
1002 // 2D tensor of f64 (2x3)
1003 let data = vec![1.1f64, 2.2, 3.3, 4.4, 5.5, 6.6];
1004 let tensor = Tensor::new(vec![2, 3], data.clone());
1005 let val = VsfType::t_f6(tensor);
1006 let flat = val.flatten();
1007
1008 let mut ptr = 0;
1009 let parsed = parse(&flat, &mut ptr).unwrap();
1010
1011 if let VsfType::t_f6(t) = parsed {
1012 assert_eq!(t.shape, vec![2, 3]);
1013 for (a, b) in t.data.iter().zip(data.iter()) {
1014 assert!((a - b).abs() < 1e-10);
1015 }
1016 } else {
1017 panic!("Expected t_f6 tensor");
1018 }
1019 }
1020
1021 #[test]
1022 #[cfg(feature = "spirix")]
1023 fn test_roundtrip_spirix_f4e3() {
1024 use spirix::{CircleF4E3, ScalarF4E3};
1025
1026 // ScalarF4E3
1027 let scalar = ScalarF4E3 {
1028 fraction: 12345,
1029 exponent: -42,
1030 };
1031 let val = VsfType::s43(scalar);
1032 let flat = val.flatten();
1033
1034 let mut ptr = 0;
1035 let parsed = parse(&flat, &mut ptr).unwrap();
1036 if let VsfType::s43(s) = parsed {
1037 assert_eq!(s.fraction, 12345);
1038 assert_eq!(s.exponent, -42);
1039 assert_eq!(ptr, flat.len()); // Consumed all bytes
1040 } else {
1041 panic!("Expected s43");
1042 }
1043
1044 // CircleF4E3
1045 let circle = CircleF4E3 {
1046 real: 100,
1047 imaginary: -200,
1048 exponent: 5,
1049 };
1050 let val = VsfType::c43(circle);
1051 let flat = val.flatten();
1052
1053 let mut ptr = 0;
1054 let parsed = parse(&flat, &mut ptr).unwrap();
1055 if let VsfType::c43(c) = parsed {
1056 assert_eq!(c.real, 100);
1057 assert_eq!(c.imaginary, -200);
1058 assert_eq!(c.exponent, 5);
1059 assert_eq!(ptr, flat.len()); // Consumed all bytes
1060 } else {
1061 panic!("Expected c43");
1062 }
1063 }
1064
1065 #[test]
1066 fn test_roundtrip_bitpacked_12bit() {
1067 use crate::types::BitPackedTensor;
1068
1069 // Lumis 12-bit RAW: small 10x20 sensor
1070 let samples: Vec<u64> = (0..200).map(|i| (i * 17) % 4096).collect(); // 12-bit values
1071 let tensor = BitPackedTensor::pack(12, vec![10, 20], &samples);
1072
1073 // Encode
1074 let val = VsfType::p(tensor);
1075 let flat = val.flatten();
1076
1077 // Decode
1078 let mut ptr = 0;
1079 let parsed = parse(&flat, &mut ptr).unwrap();
1080
1081 if let VsfType::p(decoded) = parsed {
1082 assert_eq!(decoded.bit_depth, 12);
1083 assert_eq!(decoded.shape, vec![10, 20]);
1084 assert_eq!(ptr, flat.len()); // Consumed all bytes
1085
1086 // Unpack and verify
1087 let unpacked = decoded.unpack().into_u64();
1088 assert_eq!(unpacked.len(), 200);
1089 for (i, &val) in unpacked.iter().enumerate() {
1090 assert_eq!(val, samples[i], "Sample {} mismatch", i);
1091 }
1092 } else {
1093 panic!("Expected bitpacked tensor");
1094 }
1095 }
1096
1097 #[test]
1098 fn test_roundtrip_bitpacked_1bit() {
1099 use crate::types::BitPackedTensor;
1100
1101 // 1-bit boolean-like tensor
1102 let samples: Vec<u64> = vec![1, 0, 1, 1, 0, 0, 1, 0];
1103 let tensor = BitPackedTensor::pack(1, vec![8], &samples);
1104
1105 let val = VsfType::p(tensor);
1106 let flat = val.flatten();
1107
1108 let mut ptr = 0;
1109 let parsed = parse(&flat, &mut ptr).unwrap();
1110
1111 if let VsfType::p(decoded) = parsed {
1112 assert_eq!(decoded.bit_depth, 1);
1113 assert_eq!(decoded.shape, vec![8]);
1114 assert_eq!(decoded.data.len(), 1); // 8 bits = 1 byte
1115
1116 let unpacked = decoded.unpack().into_u64();
1117 assert_eq!(unpacked, samples);
1118 } else {
1119 panic!("Expected bitpacked tensor");
1120 }
1121 }
1122
1123 #[test]
1124 fn test_roundtrip_bitpacked_13bit() {
1125 use crate::types::BitPackedTensor;
1126
1127 // 13-bit arbitrary depth
1128 let samples: Vec<u64> = vec![0, 8191, 4096, 2048, 1024];
1129 let tensor = BitPackedTensor::pack(13, vec![5], &samples);
1130
1131 let val = VsfType::p(tensor);
1132 let flat = val.flatten();
1133
1134 let mut ptr = 0;
1135 let parsed = parse(&flat, &mut ptr).unwrap();
1136
1137 if let VsfType::p(decoded) = parsed {
1138 assert_eq!(decoded.bit_depth, 13);
1139 assert_eq!(decoded.shape, vec![5]);
1140
1141 let unpacked = decoded.unpack().into_u64();
1142 assert_eq!(unpacked, samples);
1143 } else {
1144 panic!("Expected bitpacked tensor");
1145 }
1146 }
1147
1148 #[test]
1149 #[should_panic(expected = "Cannot pack")]
1150 fn test_bitpacked_type_overflow() {
1151 use crate::types::BitPackedTensor;
1152
1153 // Try to pack 12-bit values into u8 (type too small)
1154 let samples: Vec<u8> = vec![255]; // u8 only holds 8 bits, need 12
1155 BitPackedTensor::pack(12, vec![1], &samples); // Should panic: type capacity exceeded
1156 }
1157
1158 #[test]
1159 fn test_bitpacked_value_masking() {
1160 use crate::types::BitPackedTensor;
1161
1162 // Values exceeding bit_depth are masked (no panic, just truncation)
1163 let samples: Vec<u64> = vec![4096]; // 4096 = 0x1000, 12-bit max is 4095
1164 let tensor = BitPackedTensor::pack(12, vec![1], &samples); // No panic!
1165
1166 // Unpack should give masked value: 4096 & 0xFFF = 0
1167 let unpacked = tensor.unpack().into_u64();
1168 assert_eq!(unpacked[0], 0); // Low 12 bits of 4096 (0x1000) = 0
1169 }
1170
1171 #[test]
1172 fn test_world_coord_xyz_roundtrip() {
1173 use crate::types::WorldCoord;
1174
1175 // Test XYZ round-trip (simpler, no lat/lon conversion)
1176 let coord = WorldCoord::from_xyz(0.5, 0.5, 0.7071); // Normalized point
1177 let (x, y, z) = coord.to_xyz();
1178
1179 // Should be very close (Dymaxion has ~2mm error on Earth radius ~6371km)
1180 assert!((x - 0.5).abs() < 0.01, "X error: {}", (x - 0.5).abs());
1181 assert!((y - 0.5).abs() < 0.01, "Y error: {}", (y - 0.5).abs());
1182 assert!((z - 0.7071).abs() < 0.01, "Z error: {}", (z - 0.7071).abs());
1183 }
1184
1185 #[test]
1186 fn test_roundtrip_world_coord() {
1187 use crate::types::WorldCoord;
1188
1189 // Test with a simple coordinate (0, 0) - equator, prime meridian
1190 let coord = WorldCoord::from_lat_lon(0.0, 0.0);
1191 let val = VsfType::w(coord);
1192 let flat = val.flatten();
1193
1194 assert_eq!(flat[0], b'w');
1195 assert_eq!(flat[1], b'6'); // w6 = WorldCoord 64-bit standard precision
1196 assert_eq!(flat.len(), 10); // "w6" + 8 bytes for u64
1197
1198 let mut ptr = 0;
1199 let parsed = parse(&flat, &mut ptr).unwrap();
1200 assert_eq!(ptr, flat.len());
1201
1202 if let VsfType::w(decoded) = parsed {
1203 assert_eq!(decoded.raw(), coord.raw());
1204 let (lat, lon) = decoded.to_lat_lon();
1205 // Test at equator/prime meridian for simplicity
1206 println!("Decoded (0,0): lat={}, lon={}", lat, lon);
1207 assert!(lat.abs() < 1.0, "Lat error: {}", lat.abs());
1208 assert!(lon.abs() < 1.0, "Lon error: {}", lon.abs());
1209 } else {
1210 panic!("Expected WorldCoord");
1211 }
1212 }
1213
1214 #[test]
1215 fn test_world_coord_word_encoding() {
1216 use crate::types::WorldCoord;
1217
1218 // Test word encoding round-trip
1219 let coord = WorldCoord::from_lat_lon(51.5074, -0.1278); // London
1220 let words = coord.to_words();
1221
1222 // Should be 7 words
1223 assert_eq!(words.split_whitespace().count(), 7);
1224
1225 // Decode back
1226 let decoded = WorldCoord::from_words(&words).expect("Should decode valid words");
1227 assert_eq!(decoded.raw(), coord.raw());
1228 }
1229
1230 #[test]
1231 fn test_wrapped_type_roundtrip() {
1232 // Test the 'v' wrapped data type
1233 let original_data = vec![1, 2, 3, 4, 5, 6, 7, 8];
1234
1235 // Wrap with algorithm 'z' (zstd compression - simulated)
1236 let wrapped = VsfType::v(b'z', original_data.clone());
1237 let flat = wrapped.flatten();
1238
1239 // Verify encoding
1240 assert_eq!(flat[0], b'v'); // Marker
1241 assert_eq!(flat[1], b'z'); // Algorithm
1242
1243 // Parse back
1244 let mut ptr = 0;
1245 let parsed = parse(&flat, &mut ptr).unwrap();
1246
1247 if let VsfType::v(alg, data) = parsed {
1248 assert_eq!(alg, b'z');
1249 assert_eq!(data, original_data);
1250 assert_eq!(ptr, flat.len()); // Consumed all bytes
1251 } else {
1252 panic!("Expected wrapped type");
1253 }
1254 }
1255
1256 #[test]
1257 fn test_wrapped_type_algorithms() {
1258 // Test different algorithm identifiers
1259 let algorithms = vec![b'z', b'r', b'x', b'e'];
1260 let test_data = vec![0xAB; 100];
1261
1262 for alg in algorithms {
1263 let wrapped = VsfType::v(alg, test_data.clone());
1264 let flat = wrapped.flatten();
1265
1266 let mut ptr = 0;
1267 let parsed = parse(&flat, &mut ptr).unwrap();
1268
1269 if let VsfType::v(parsed_alg, parsed_data) = parsed {
1270 assert_eq!(parsed_alg, alg);
1271 assert_eq!(parsed_data, test_data);
1272 } else {
1273 panic!("Expected wrapped type for algorithm '{}'", alg as char);
1274 }
1275 }
1276 }
1277}