1use crate::{
10 bitpack::unpack_indices,
11 codec::{FibCodeV1, CODE_SCHEMA},
12 profile::{FibQuantProfileV1, NormFormat},
13 FibQuantError, Result,
14};
15
16pub const WIRE_MAGIC: [u8; 4] = [b'F', b'B', b'W', b'1'];
18pub const WIRE_VERSION: u8 = 1;
20pub const WIRE_HEADER_SIZE: usize = 59;
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct WireHeader {
28 pub version: u8,
29 pub ambient_dim: u32,
30 pub block_dim: u32,
31 pub codebook_size: u32,
32 pub rotation_seed: u64,
33 pub wire_index_bits: u8,
34 pub norm_format: NormFormat,
35 pub profile_digest: [u8; 32],
36 pub body_offset: usize,
37}
38
39pub struct FibCodeWireV1;
41
42impl FibCodeWireV1 {
43 pub fn to_wire_bytes(code: &FibCodeV1, profile: &FibQuantProfileV1) -> Result<Vec<u8>> {
47 let profile_digest_hex = profile.digest()?;
48 let digest_bytes = hex_decode_32(strip_blake3_prefix(&profile_digest_hex))?;
49 let mut out =
50 Vec::with_capacity(WIRE_HEADER_SIZE + 2 + code.norm_payload.len() + code.indices.len());
51
52 out.extend_from_slice(&WIRE_MAGIC);
54 out.push(WIRE_VERSION);
55 out.extend_from_slice(&profile.ambient_dim.to_le_bytes());
56 out.extend_from_slice(&profile.block_dim.to_le_bytes());
57 out.extend_from_slice(&profile.codebook_size.to_le_bytes());
58 out.extend_from_slice(&profile.rotation_seed.to_le_bytes());
59 out.push(profile.wire_index_bits);
60 out.push(norm_format_to_byte(&profile.norm_format));
61 out.extend_from_slice(&digest_bytes);
62
63 let norm_len = code.norm_payload.len() as u16;
65 out.extend_from_slice(&norm_len.to_le_bytes());
66 out.extend_from_slice(&code.norm_payload);
67 out.extend_from_slice(&code.indices);
68 Ok(out)
69 }
70
71 pub fn parse_header(bytes: &[u8]) -> Result<WireHeader> {
74 if bytes.len() < WIRE_HEADER_SIZE {
75 return Err(FibQuantError::CorruptPayload(format!(
76 "wire format too short: {} bytes (need >= {})",
77 bytes.len(),
78 WIRE_HEADER_SIZE
79 )));
80 }
81 if bytes[0..4] != WIRE_MAGIC {
82 return Err(FibQuantError::CorruptPayload(format!(
83 "wire bad magic: {:?} (expected {:?})",
84 &bytes[0..4],
85 WIRE_MAGIC
86 )));
87 }
88 let version = bytes[4];
89 if version != WIRE_VERSION {
90 return Err(FibQuantError::CorruptPayload(format!(
91 "wire version {} not supported (need {})",
92 version, WIRE_VERSION
93 )));
94 }
95 let ambient_dim = u32::from_le_bytes([bytes[5], bytes[6], bytes[7], bytes[8]]);
96 let block_dim = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
97 let codebook_size = u32::from_le_bytes([bytes[13], bytes[14], bytes[15], bytes[16]]);
98 let rotation_seed = u64::from_le_bytes([
99 bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], bytes[24],
100 ]);
101 let wire_index_bits = bytes[25];
102 let norm_format = byte_to_norm_format(bytes[26])?;
103 let mut profile_digest = [0u8; 32];
104 profile_digest.copy_from_slice(&bytes[27..59]);
105
106 Ok(WireHeader {
107 version,
108 ambient_dim,
109 block_dim,
110 codebook_size,
111 rotation_seed,
112 wire_index_bits,
113 norm_format,
114 profile_digest,
115 body_offset: WIRE_HEADER_SIZE,
116 })
117 }
118
119 pub fn from_wire_bytes(bytes: &[u8]) -> Result<(FibCodeV1, FibQuantProfileV1)> {
123 let header = Self::parse_header(bytes)?;
124 let body = &bytes[header.body_offset..];
125 if body.len() < 2 {
126 return Err(FibQuantError::CorruptPayload(
127 "wire body too short for norm_len".into(),
128 ));
129 }
130 let norm_len = u16::from_le_bytes([body[0], body[1]]) as usize;
131 if body.len() < 2 + norm_len {
132 return Err(FibQuantError::CorruptPayload(format!(
133 "wire norm truncated: norm_len={} but only {} bytes remain",
134 norm_len,
135 body.len() - 2
136 )));
137 }
138 let norm_payload = body[2..2 + norm_len].to_vec();
139 let indices = body[2 + norm_len..].to_vec();
140
141 let profile = FibQuantProfileV1::paper_default(
143 header.ambient_dim as usize,
144 header.block_dim as usize,
145 header.codebook_size as usize,
146 header.rotation_seed,
147 )?;
148
149 if profile.wire_index_bits != header.wire_index_bits {
151 return Err(FibQuantError::CorruptPayload(format!(
152 "wire_index_bits {} does not match expected {}",
153 header.wire_index_bits, profile.wire_index_bits
154 )));
155 }
156
157 let block_count = profile.block_count() as usize;
159 let expected_packed_len = (block_count * header.wire_index_bits as usize).div_ceil(8);
160 if indices.len() != expected_packed_len {
161 return Err(FibQuantError::CorruptPayload(format!(
162 "wire indices: got {} bytes, expected {}",
163 indices.len(),
164 expected_packed_len
165 )));
166 }
167
168 let _ = unpack_indices(&indices, block_count, header.wire_index_bits)?;
170
171 let profile_digest = profile.digest()?;
172 Ok((
173 FibCodeV1 {
174 schema_version: CODE_SCHEMA.into(),
175 profile_digest,
176 codebook_digest: String::new(),
177 rotation_digest: String::new(),
178 ambient_dim: profile.ambient_dim,
179 block_dim: profile.block_dim,
180 norm_format: profile.norm_format.clone(),
181 norm_payload,
182 wire_index_bits: header.wire_index_bits,
183 block_count: profile.block_count(),
184 indices,
185 },
186 profile,
187 ))
188 }
189}
190
191fn norm_format_to_byte(format: &NormFormat) -> u8 {
192 match format {
193 NormFormat::Fp16Paper => 0,
194 NormFormat::F32Reference => 1,
195 }
196}
197
198fn byte_to_norm_format(byte: u8) -> Result<NormFormat> {
199 match byte {
200 0 => Ok(NormFormat::Fp16Paper),
201 1 => Ok(NormFormat::F32Reference),
202 _ => Err(FibQuantError::CorruptPayload(format!(
203 "unknown norm_format byte: {}",
204 byte
205 ))),
206 }
207}
208
209fn hex_decode_32(hex: &str) -> Result<[u8; 32]> {
210 if hex.len() != 64 {
211 return Err(FibQuantError::CorruptPayload(format!(
212 "digest hex length {} (expected 64)",
213 hex.len()
214 )));
215 }
216 let mut out = [0u8; 32];
217 for i in 0..32 {
218 out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
219 .map_err(|_| FibQuantError::CorruptPayload("invalid hex digest".into()))?;
220 }
221 Ok(out)
222}
223
224fn strip_blake3_prefix(digest: &str) -> &str {
226 digest.strip_prefix("blake3:").unwrap_or(digest)
227}
228
229#[allow(dead_code)]
230fn hex_encode_32(bytes: &[u8; 32]) -> String {
231 let mut out = String::with_capacity(64);
232 for b in bytes.iter() {
233 out.push_str(&format!("{:02x}", b));
234 }
235 out
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::{codec::FibQuantizer, profile::FibQuantProfileV1};
242
243 fn build_test() -> Result<(FibQuantizer, FibQuantProfileV1)> {
244 let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 7)?;
245 profile.training_samples = 128;
246 profile.lloyd_restarts = 1;
247 profile.lloyd_iterations = 2;
248 let q = FibQuantizer::new(profile.clone())?;
249 Ok((q, profile))
250 }
251
252 #[test]
253 fn wire_roundtrip_preserves_data() -> Result<()> {
254 let (q, profile) = build_test()?;
255 let input = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
256 let code = q.encode(&input)?;
257 let wire = FibCodeWireV1::to_wire_bytes(&code, &profile)?;
258 let (decoded_code, decoded_profile) = FibCodeWireV1::from_wire_bytes(&wire)?;
259 assert_eq!(decoded_code.indices, code.indices);
260 assert_eq!(decoded_code.norm_payload, code.norm_payload);
261 assert_eq!(decoded_code.wire_index_bits, code.wire_index_bits);
262 assert_eq!(decoded_code.block_count, code.block_count);
263 assert_eq!(decoded_profile.ambient_dim, profile.ambient_dim);
264 assert_eq!(decoded_profile.block_dim, profile.block_dim);
265 assert_eq!(decoded_profile.codebook_size, profile.codebook_size);
266 assert_eq!(decoded_profile.rotation_seed, profile.rotation_seed);
267 Ok(())
268 }
269
270 #[test]
271 fn wire_parse_header_extracts_metadata() -> Result<()> {
272 let (q, profile) = build_test()?;
273 let input = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
274 let code = q.encode(&input)?;
275 let wire = FibCodeWireV1::to_wire_bytes(&code, &profile)?;
276 let header = FibCodeWireV1::parse_header(&wire)?;
277 assert_eq!(header.version, WIRE_VERSION);
278 assert_eq!(header.ambient_dim, 8);
279 assert_eq!(header.block_dim, 2);
280 assert_eq!(header.codebook_size, 8);
281 assert_eq!(header.rotation_seed, 7);
282 assert_eq!(header.wire_index_bits, profile.wire_index_bits);
283 assert_eq!(header.body_offset, WIRE_HEADER_SIZE);
284 Ok(())
285 }
286
287 #[test]
288 fn wire_rejects_bad_magic() -> Result<()> {
289 let result = FibCodeWireV1::parse_header(b"XXXX");
290 assert!(result.is_err());
291 Ok(())
292 }
293
294 #[test]
295 fn wire_rejects_truncation() -> Result<()> {
296 let result = FibCodeWireV1::parse_header(&[0; 10]);
297 assert!(result.is_err());
298 Ok(())
299 }
300
301 #[test]
302 fn wire_decoded_code_roundtrips_through_quantizer() -> Result<()> {
303 let (q, profile) = build_test()?;
304 let input = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
305 let code = q.encode(&input)?;
306 let wire = FibCodeWireV1::to_wire_bytes(&code, &profile)?;
307 let (decoded_code, decoded_profile) = FibCodeWireV1::from_wire_bytes(&wire)?;
308 let q2 = FibQuantizer::new(decoded_profile)?;
309 let decoded = q2.decode(&decoded_code)?;
310 assert_eq!(decoded.len(), input.len());
311 for (a, b) in input.iter().zip(decoded.iter()) {
312 assert!(a.is_finite());
313 assert!(b.is_finite());
314 }
315 Ok(())
316 }
317
318 #[test]
319 fn wire_corrupt_dim_is_error() -> Result<()> {
320 let (q, profile) = build_test()?;
321 let input = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
322 let code = q.encode(&input)?;
323 let mut wire = FibCodeWireV1::to_wire_bytes(&code, &profile)?;
324 wire[5] ^= 0xFF;
326 let result = FibCodeWireV1::from_wire_bytes(&wire);
327 let _ = result;
331 Ok(())
332 }
333}