Skip to main content

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, &section_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}