1use crate::codec::column_codec::{
11 decode_attr_col, decode_row_kinds, decode_surrogates, decode_timestamps_col,
12};
13use crate::codec::coord_rle::decode_coord_axis_rle;
14use crate::codec::limits::{
15 MAX_ATTRS_PER_TILE, MAX_AXES_PER_TILE, MAX_CELLS_PER_TILE, check_decoded_size, checked_capacity,
16};
17use crate::codec::tag::{CodecTag, peek_tag};
18use crate::error::{ArrayError, ArrayResult};
19use crate::tile::mbr::TileMBR;
20use crate::tile::sparse_tile::{RowKind, SparseTile};
21
22const SUPPORTED_PAYLOAD_VERSION: u8 = 1;
23
24fn read_framed<'a>(data: &'a [u8], pos: &mut usize) -> ArrayResult<&'a [u8]> {
25 if *pos + 4 > data.len() {
26 return Err(ArrayError::SegmentCorruption {
27 detail: "framed block: truncated length".into(),
28 });
29 }
30 let len = u32::from_le_bytes(
31 data[*pos..*pos + 4]
32 .try_into()
33 .expect("invariant: bounds-checked above (*pos + 4 <= data.len())"),
34 ) as usize;
35 *pos += 4;
36 if *pos + len > data.len() {
37 return Err(ArrayError::SegmentCorruption {
38 detail: format!(
39 "framed block: body truncated (need {len}, have {})",
40 data.len() - *pos
41 ),
42 });
43 }
44 let slice = &data[*pos..*pos + len];
45 *pos += len;
46 Ok(slice)
47}
48
49pub fn decode_sparse_tile(payload: &[u8]) -> ArrayResult<SparseTile> {
54 if payload.len() < 2 {
55 return Err(ArrayError::SegmentCorruption {
56 detail: "sparse tile payload too short".into(),
57 });
58 }
59
60 let tag_result = peek_tag(payload).ok_or_else(|| {
61 ArrayError::SegmentCorruption {
64 detail: format!(
65 "decode_sparse_tile called with legacy or unknown tag byte: {:#04x}",
66 payload[0]
67 ),
68 }
69 })?;
70
71 let version = payload[1];
72 if version != SUPPORTED_PAYLOAD_VERSION {
73 return Err(ArrayError::SegmentCorruption {
74 detail: format!("unsupported tile payload version: {version}"),
75 });
76 }
77
78 match tag_result {
79 CodecTag::Raw => decode_raw(&payload[2..]),
80 CodecTag::Structural => decode_structural(&payload[2..]),
81 }
82}
83
84fn decode_raw(body: &[u8]) -> ArrayResult<SparseTile> {
85 let tile: SparseTile =
86 zerompk::from_msgpack(body).map_err(|e| ArrayError::SegmentCorruption {
87 detail: format!("raw tile decode: {e}"),
88 })?;
89 tile.check_stored_identities()?;
90 Ok(tile)
91}
92
93fn decode_structural(body: &[u8]) -> ArrayResult<SparseTile> {
94 let mut pos = 0;
95
96 if pos + 8 > body.len() {
97 return Err(ArrayError::SegmentCorruption {
98 detail: "structural tile: truncated counts".into(),
99 });
100 }
101 let cell_count = u32::from_le_bytes(
102 body[pos..pos + 4]
103 .try_into()
104 .expect("invariant: bounds-checked above (pos + 8 <= body.len())"),
105 ) as usize;
106 pos += 4;
107 check_decoded_size(cell_count, MAX_CELLS_PER_TILE, "cell_count")?;
108 let axis_count = u32::from_le_bytes(
109 body[pos..pos + 4]
110 .try_into()
111 .expect("invariant: bounds-checked above (pos + 4 <= body.len())"),
112 ) as usize;
113 pos += 4;
114 let axis_capacity = checked_capacity(
115 axis_count,
116 MAX_AXES_PER_TILE,
117 body.len() - pos,
118 4,
119 "axis_count",
120 )?;
121
122 let mut dim_dicts = Vec::with_capacity(axis_capacity);
124 for _ in 0..axis_count {
125 let axis_bytes = read_framed(body, &mut pos)?;
126 let mut inner_pos = 0;
127 let dict = decode_coord_axis_rle(axis_bytes, &mut inner_pos)?;
128 dim_dicts.push(dict);
129 }
130
131 let surr_bytes = read_framed(body, &mut pos)?;
133 let live_surrogates = decode_surrogates(surr_bytes)?;
134
135 let rk_bytes = read_framed(body, &mut pos)?;
137 let row_kinds = decode_row_kinds(rk_bytes)?;
138
139 let _sys_bytes = read_framed(body, &mut pos)?;
141
142 let vf_bytes = read_framed(body, &mut pos)?;
144 let valid_from_ms = decode_timestamps_col(vf_bytes)?;
145
146 let vu_bytes = read_framed(body, &mut pos)?;
148 let valid_until_ms = decode_timestamps_col(vu_bytes)?;
149
150 if pos + 4 > body.len() {
152 return Err(ArrayError::SegmentCorruption {
153 detail: "structural tile: truncated attr count".into(),
154 });
155 }
156 let attr_count = u32::from_le_bytes(
157 body[pos..pos + 4]
158 .try_into()
159 .expect("invariant: bounds-checked above (pos + 4 <= body.len())"),
160 ) as usize;
161 pos += 4;
162 let attr_capacity = checked_capacity(
163 attr_count,
164 MAX_ATTRS_PER_TILE,
165 body.len() - pos,
166 4,
167 "attr_count",
168 )?;
169
170 let mut attr_cols = Vec::with_capacity(attr_capacity);
171 for _ in 0..attr_count {
172 let col_bytes = read_framed(body, &mut pos)?;
173 let col = decode_attr_col(col_bytes)?;
174 attr_cols.push(col);
175 }
176
177 let mbr = TileMBR::new(axis_count, attr_count);
182
183 if row_kinds.len() != cell_count {
185 return Err(ArrayError::SegmentCorruption {
186 detail: format!(
187 "structural tile: row kind count {kinds} != cell_count {cell_count}",
188 kinds = row_kinds.len()
189 ),
190 });
191 }
192 let surrogates = spread_live_surrogates(&row_kinds, live_surrogates)?;
193
194 let tile = SparseTile {
195 dim_dicts,
196 attr_cols,
197 surrogates,
198 valid_from_ms,
199 valid_until_ms,
200 row_kinds,
201 mbr,
202 };
203 tile.check_stored_identities()?;
204 Ok(tile)
205}
206
207fn spread_live_surrogates(
211 row_kinds: &[u8],
212 live_surrogates: Vec<nodedb_types::Surrogate>,
213) -> ArrayResult<Vec<Option<nodedb_types::Surrogate>>> {
214 let live_count = live_surrogates.len();
215 let mut live = live_surrogates.into_iter();
216 let mut surrogates = Vec::with_capacity(row_kinds.len());
217 for (row, &kind) in row_kinds.iter().enumerate() {
218 if RowKind::from_u8(kind)? == RowKind::Live {
219 let surrogate = live.next().ok_or_else(|| ArrayError::SegmentCorruption {
220 detail: format!(
221 "structural tile: live row {row} has no surrogate ({live_count} stored)"
222 ),
223 })?;
224 surrogates.push(Some(surrogate));
225 } else {
226 surrogates.push(None);
227 }
228 }
229 if live.next().is_some() {
230 return Err(ArrayError::SegmentCorruption {
231 detail: format!("structural tile: {live_count} surrogates stored for fewer live rows"),
232 });
233 }
234 Ok(surrogates)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::codec::tile_encode::encode_sparse_tile;
241 use crate::schema::ArraySchemaBuilder;
242 use crate::schema::attr_spec::{AttrSpec, AttrType};
243 use crate::schema::dim_spec::{DimSpec, DimType};
244 use crate::tile::sparse_tile::{RowKind, SparseRow, SparseTileBuilder};
245 use crate::types::cell_value::value::CellValue;
246 use crate::types::coord::value::CoordValue;
247 use crate::types::domain::{Domain, DomainBound};
248 use nodedb_types::{OPEN_UPPER, Surrogate};
249
250 fn schema() -> crate::schema::ArraySchema {
251 ArraySchemaBuilder::new("t")
252 .dim(DimSpec::new(
253 "x",
254 DimType::Int64,
255 Domain::new(DomainBound::Int64(0), DomainBound::Int64(1_000_000)),
256 ))
257 .attr(AttrSpec::new("v", AttrType::Int64, true))
258 .tile_extents(vec![1000])
259 .build()
260 .unwrap()
261 }
262
263 fn make_tile(s: &crate::schema::ArraySchema, n: usize) -> SparseTile {
264 let mut b = SparseTileBuilder::new(s);
265 for i in 0..n {
266 b.push_row(SparseRow {
267 coord: &[CoordValue::Int64(i as i64)],
268 attrs: &[CellValue::Int64(i as i64 * 2)],
269 surrogate: Some(Surrogate::new(i as u32 + 1)),
270 valid_from_ms: i as i64,
271 valid_until_ms: OPEN_UPPER,
272 kind: RowKind::Live,
273 })
274 .unwrap();
275 }
276 b.build()
277 }
278
279 fn roundtrip(tile: &SparseTile) -> SparseTile {
280 let mut buf = Vec::new();
281 encode_sparse_tile(tile, &mut buf).unwrap();
282 decode_sparse_tile(&buf).unwrap()
283 }
284
285 #[test]
286 fn empty_tile_roundtrip() {
287 let s = schema();
288 let tile = SparseTile::empty(&s);
289 let out = roundtrip(&tile);
290 assert_eq!(out.surrogates, tile.surrogates);
291 assert_eq!(out.row_kinds, tile.row_kinds);
292 }
293
294 #[test]
295 fn small_tile_roundtrip() {
296 let s = schema();
297 let tile = make_tile(&s, 4);
298 let out = roundtrip(&tile);
299 assert_eq!(out.valid_from_ms, tile.valid_from_ms);
300 assert_eq!(out.attr_cols, tile.attr_cols);
301 }
302
303 #[test]
304 fn structural_tile_roundtrip() {
305 let s = schema();
306 let tile = make_tile(&s, 50);
307 let out = roundtrip(&tile);
308 assert_eq!(out.surrogates.len(), tile.surrogates.len());
309 assert_eq!(out.valid_from_ms, tile.valid_from_ms);
310 assert_eq!(out.valid_until_ms, tile.valid_until_ms);
311 assert_eq!(out.attr_cols, tile.attr_cols);
312 assert_eq!(out.row_kinds, tile.row_kinds);
313 }
314
315 #[test]
316 fn one_thousand_cells_roundtrip() {
317 let s = schema();
318 let tile = make_tile(&s, 1000);
319 let out = roundtrip(&tile);
320 assert_eq!(out.surrogates.len(), 1000);
321 assert_eq!(out.dim_dicts[0].indices, tile.dim_dicts[0].indices);
322 }
323
324 #[test]
325 fn tombstone_rows_roundtrip() {
326 let s = schema();
327 let mut b = SparseTileBuilder::new(&s);
328 for i in 0..20 {
329 b.push_row(SparseRow {
330 coord: &[CoordValue::Int64(i)],
331 attrs: &[CellValue::Int64(i)],
332 surrogate: Some(Surrogate::new(i as u32 + 1)),
333 valid_from_ms: 0,
334 valid_until_ms: OPEN_UPPER,
335 kind: RowKind::Live,
336 })
337 .unwrap();
338 }
339 b.push_row(SparseRow {
340 coord: &[CoordValue::Int64(99)],
341 attrs: &[],
342 surrogate: None,
343 valid_from_ms: 0,
344 valid_until_ms: OPEN_UPPER,
345 kind: RowKind::Tombstone,
346 })
347 .unwrap();
348 let tile = b.build();
349 let out = roundtrip(&tile);
350 assert_eq!(out.row_kinds, tile.row_kinds);
351 assert_eq!(out.surrogates, tile.surrogates);
352 assert_eq!(out.surrogates[20], None);
353 }
354
355 #[test]
358 fn a_live_row_without_a_stored_surrogate_is_refused() {
359 let kinds = [
360 RowKind::Live.as_u8(),
361 RowKind::Tombstone.as_u8(),
362 RowKind::Live.as_u8(),
363 ];
364 assert!(spread_live_surrogates(&kinds, vec![Surrogate::new(1)]).is_err());
365 assert!(
366 spread_live_surrogates(
367 &kinds,
368 vec![Surrogate::new(1), Surrogate::new(2), Surrogate::new(3)]
369 )
370 .is_err(),
371 "a surrogate left over after the last live row is refused"
372 );
373 assert_eq!(
374 spread_live_surrogates(&kinds, vec![Surrogate::new(1), Surrogate::new(2)]).unwrap(),
375 vec![Some(Surrogate::new(1)), None, Some(Surrogate::new(2))]
376 );
377 }
378
379 #[test]
380 fn huge_axis_count_in_tiny_structural_payload_is_rejected_before_allocation() {
381 let mut body = Vec::new();
382 body.extend_from_slice(&0u32.to_le_bytes());
383 body.extend_from_slice(&(MAX_AXES_PER_TILE as u32).to_le_bytes());
384 let error = decode_structural(&body).unwrap_err();
385 assert!(matches!(error, ArrayError::SegmentCorruption { .. }));
386 }
387
388 #[test]
389 fn invalid_version_returns_error() {
390 let s = schema();
391 let tile = make_tile(&s, 20);
392 let mut buf = Vec::new();
393 encode_sparse_tile(&tile, &mut buf).unwrap();
394 buf[1] = 99;
396 let err = decode_sparse_tile(&buf).unwrap_err();
397 assert!(matches!(err, ArrayError::SegmentCorruption { .. }));
398 }
399
400 #[test]
401 fn valid_time_variants_roundtrip() {
402 let s = schema();
403 let mut b = SparseTileBuilder::new(&s);
404 b.push_row(SparseRow {
405 coord: &[CoordValue::Int64(1)],
406 attrs: &[CellValue::Int64(10)],
407 surrogate: Some(Surrogate::new(1)),
408 valid_from_ms: 100,
409 valid_until_ms: 500,
410 kind: RowKind::Live,
411 })
412 .unwrap();
413 b.push_row(SparseRow {
414 coord: &[CoordValue::Int64(2)],
415 attrs: &[CellValue::Int64(20)],
416 surrogate: Some(Surrogate::new(2)),
417 valid_from_ms: 200,
418 valid_until_ms: OPEN_UPPER,
419 kind: RowKind::Live,
420 })
421 .unwrap();
422 for i in 3..20 {
424 b.push_row(SparseRow {
425 coord: &[CoordValue::Int64(i)],
426 attrs: &[CellValue::Int64(i)],
427 surrogate: Some(Surrogate::new(i as u32)),
428 valid_from_ms: i * 10,
429 valid_until_ms: OPEN_UPPER,
430 kind: RowKind::Live,
431 })
432 .unwrap();
433 }
434 let tile = b.build();
435 let out = roundtrip(&tile);
436 assert_eq!(out.valid_from_ms[0], 100);
437 assert_eq!(out.valid_until_ms[0], 500);
438 assert_eq!(out.valid_until_ms[1], OPEN_UPPER);
439 }
440}