Skip to main content

edgefirst_tensor/
blob.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! The tensor blob: a size-prefixed, append-only serialization of a tensor.
5//!
6//! # Why a blob and not the descriptor
7//!
8//! [`TensorDesc`](crate::TensorDesc) is a fixed C struct with one handle and no
9//! plane table. The blob carries every plane explicitly, in either of two
10//! transport modes, and survives being appended to.
11//!
12//! # Layout
13//!
14//! **Region order is permanent from first publication.** The format is
15//! size-prefixed and append-only: a new field may only be appended, never
16//! inserted, and `required_mask` covers the one case appending cannot — an
17//! addition that changes how existing fields are *interpreted*, where an
18//! unaware consumer must refuse rather than proceed.
19//!
20//! The fixed header is 64 bytes, laid out widest-first so every scalar is
21//! naturally aligned and no reader performs an unaligned access:
22//!
23//! | Offset | Type | Field |
24//! |---|---|---|
25//! | 0 | `u64` | `size` — total blob bytes |
26//! | 8 | `u64` | `required_mask` |
27//! | 16 | `u64` | `planes_bytes` |
28//! | 24 | `u32` | `storage_kind` |
29//! | 28 | `u32` | `pid` |
30//! | 32 | `i32` | `fence_fd` |
31//! | 36 | `u32` | `dtype` |
32//! | 40 | `i32` | `quant_axis` |
33//! | 44 | `u32` | `ndim` |
34//! | 48 | `u32` | `plane_count` |
35//! | 52 | `u32` | `quant_scales_len` |
36//! | 56 | `u32` | `quant_zero_points_len` |
37//! | 60 | `u32` | `strings_bytes` |
38//! | 64 | `u32` | `strides_len` — `ndim` entries, or 0 for densely packed |
39//! | 68 | `u32` | *reserved, must be zero* — keeps the tail 8-aligned |
40//!
41//! `strides_len` is carried rather than assumed because empty strides are a
42//! distinct, meaningful value: `Tensor.msg` defines an empty array as "densely
43//! packed C-order". Always writing `ndim` strides would be simpler but would
44//! turn a message with empty strides into a blob with explicit ones, so the
45//! reference-mode round-trip would no longer be the identity.
46//!
47//! Because every variable region's length lives in the fixed header, each
48//! region's offset is a constant-time arithmetic derivation. This is the one
49//! thing CDR cannot do — it has no offset table, so a message's variable
50//! fields must be walked sequentially — and it is why the blob is worth having
51//! alongside the message rather than simply reusing it.
52//!
53//! Every integer is little-endian on every host, read and written explicitly.
54//! The blob is never `transmute`d from a struct: that would bake in host byte
55//! order and padding, and this format crosses hosts.
56
57/// Bytes in the fixed header. See the module docs for the field table.
58pub const HEADER_LEN: usize = 72;
59
60/// Bytes in a plane record's scalar block, before its variable payloads.
61///
62/// Matches the schemas spec's independently-stated `TensorPlane` minimum: a
63/// 48-byte scalar block (six 8-byte fields) plus two 4-byte counts.
64pub const PLANE_RECORD_LEN: usize = 56;
65
66/// `required_mask` bits this build understands. Zero: no interpretation-changing
67/// field has been added yet, so *any* bit set means a producer needs something
68/// from us that we cannot provide, and we must refuse.
69pub const SUPPORTED_REQUIRED_MASK: u64 = 0;
70
71/// File-descriptor slot in the blob's accompanying table.
72///
73/// `std::os::fd` exists only on Unix. Windows still type-checks `export` /
74/// `import` so the crate builds; the table is unused there (no dma-buf / SHM).
75#[cfg(unix)]
76type BlobFd = std::os::fd::RawFd;
77#[cfg(not(unix))]
78type BlobFd = i32;
79
80/// What went wrong parsing a blob.
81///
82/// Every variant is a *refusal*, never a best-effort recovery: a blob arrives
83/// from another wheel, another process, or a network, so every count and length
84/// in its header is attacker-controlled. Recovering from a bad one means acting
85/// on a value an attacker chose.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum BlobError {
88    /// The buffer is shorter than the structure it claims to contain.
89    Truncated {
90        /// Bytes the blob says it needs.
91        need: usize,
92        /// Bytes actually available.
93        have: usize,
94    },
95    /// `required_mask` carries a bit this build does not understand. The
96    /// producer needs an interpretation we cannot provide, so proceeding would
97    /// silently misread the data.
98    UnsupportedRequirement(u64),
99    /// A count or length would overflow when multiplied out, or derives an
100    /// offset past the end of the blob.
101    ForgedLength {
102        /// Which header field was implausible.
103        field: &'static str,
104        /// The value it carried.
105        value: u64,
106    },
107    /// A string region is not valid UTF-8.
108    InvalidUtf8(&'static str),
109    /// The blob is internally inconsistent in a way no single field explains.
110    Malformed(&'static str),
111}
112
113impl std::fmt::Display for BlobError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            Self::Truncated { need, have } => {
117                write!(f, "blob truncated: needs {need} bytes, buffer has {have}")
118            }
119            Self::UnsupportedRequirement(m) => write!(
120                f,
121                "blob requires unsupported features (required_mask {m:#x}); \
122                 refusing rather than misreading it"
123            ),
124            Self::ForgedLength { field, value } => {
125                write!(f, "blob field `{field}` has implausible value {value}")
126            }
127            Self::InvalidUtf8(which) => write!(f, "blob string `{which}` is not valid UTF-8"),
128            Self::Malformed(why) => write!(f, "malformed blob: {why}"),
129        }
130    }
131}
132
133impl std::error::Error for BlobError {}
134
135/// The blob's fixed header. See the module docs for the byte layout.
136///
137/// Field order here mirrors the byte layout so the two can be read side by
138/// side, but this struct is never cast to or from bytes — see [`Self::write_to`]
139/// and [`parse_header`], which go field by field in explicit little-endian.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141pub struct BlobHeader {
142    /// Total bytes of this blob, header included. The size prefix that makes
143    /// the format append-only: an older consumer reads the prefix it knows and
144    /// skips to `size`.
145    pub size: u64,
146    /// Bits describing additions that change interpretation rather than adding
147    /// information. Any bit a consumer does not recognise means refuse.
148    pub required_mask: u64,
149    /// Byte length of the planes region.
150    pub planes_bytes: u64,
151    /// Backing store for the whole tensor; the shared `TensorMemory` code.
152    pub storage_kind: u32,
153    /// Producer pid, for `pidfd_open`/`pidfd_getfd`. Zero when all planes are
154    /// inlined, where there is no handle to reopen.
155    pub pid: u32,
156    /// Acquire fence, or `-1` for none. Per-tensor, never per-plane.
157    pub fence_fd: i32,
158    /// Element type; the shared `DType` code.
159    pub dtype: u32,
160    /// Per-channel quantization axis; `-1` per-tensor, `-2` none.
161    pub quant_axis: i32,
162    /// Number of dimensions in the addressing grid.
163    pub ndim: u32,
164    /// Number of plane records in the planes region.
165    pub plane_count: u32,
166    /// Number of `f32` quantization scales.
167    pub quant_scales_len: u32,
168    /// Number of `i32` zero points; zero means symmetric.
169    pub quant_zero_points_len: u32,
170    /// Byte length of the five-string region.
171    pub strings_bytes: u32,
172    /// Number of stride entries: `ndim`, or 0 meaning densely packed C-order.
173    ///
174    /// Carried rather than assumed — an empty stride array is a distinct value
175    /// per `Tensor.msg`, not an absent one, and flattening the two would break
176    /// the reference-mode round-trip identity.
177    pub strides_len: u32,
178}
179
180impl BlobHeader {
181    /// An all-zero header, for tests and for `..` update syntax.
182    pub fn empty() -> Self {
183        Self::default()
184    }
185
186    /// Serialize into the first [`HEADER_LEN`] bytes of `buf`.
187    ///
188    /// # Panics
189    /// If `buf` is shorter than [`HEADER_LEN`].
190    pub fn write_to(&self, buf: &mut [u8]) {
191        assert!(buf.len() >= HEADER_LEN, "header buffer too small");
192        buf[0..8].copy_from_slice(&self.size.to_le_bytes());
193        buf[8..16].copy_from_slice(&self.required_mask.to_le_bytes());
194        buf[16..24].copy_from_slice(&self.planes_bytes.to_le_bytes());
195        buf[24..28].copy_from_slice(&self.storage_kind.to_le_bytes());
196        buf[28..32].copy_from_slice(&self.pid.to_le_bytes());
197        buf[32..36].copy_from_slice(&self.fence_fd.to_le_bytes());
198        buf[36..40].copy_from_slice(&self.dtype.to_le_bytes());
199        buf[40..44].copy_from_slice(&self.quant_axis.to_le_bytes());
200        buf[44..48].copy_from_slice(&self.ndim.to_le_bytes());
201        buf[48..52].copy_from_slice(&self.plane_count.to_le_bytes());
202        buf[52..56].copy_from_slice(&self.quant_scales_len.to_le_bytes());
203        buf[56..60].copy_from_slice(&self.quant_zero_points_len.to_le_bytes());
204        buf[60..64].copy_from_slice(&self.strings_bytes.to_le_bytes());
205        buf[64..68].copy_from_slice(&self.strides_len.to_le_bytes());
206        buf[68..72].copy_from_slice(&0u32.to_le_bytes()); // reserved
207    }
208}
209
210/// Read a little-endian `u64` at `off`. The caller has already bounds-checked.
211fn rd_u64(buf: &[u8], off: usize) -> u64 {
212    let mut b = [0u8; 8];
213    b.copy_from_slice(&buf[off..off + 8]);
214    u64::from_le_bytes(b)
215}
216
217/// Read a little-endian `u32` at `off`. The caller has already bounds-checked.
218fn rd_u32(buf: &[u8], off: usize) -> u32 {
219    let mut b = [0u8; 4];
220    b.copy_from_slice(&buf[off..off + 4]);
221    u32::from_le_bytes(b)
222}
223
224/// Parse and validate a blob's fixed header.
225///
226/// `buf` is the blob, which need not start at offset 0 of its containing
227/// allocation — a `CameraFrame` embeds a `Tensor` at an 8-aligned offset
228/// precisely so a consumer can subslice and parse in place.
229///
230/// Validates only what the header alone can prove: that the buffer holds a
231/// header at all, that this build understands every required bit, and that
232/// `size` is self-consistent. Region bounds are checked when regions are
233/// derived, against this `size`.
234pub fn parse_header(buf: &[u8]) -> Result<BlobHeader, BlobError> {
235    if buf.len() < HEADER_LEN {
236        return Err(BlobError::Truncated {
237            need: HEADER_LEN,
238            have: buf.len(),
239        });
240    }
241    let required_mask = rd_u64(buf, 8);
242    // Checked before anything else is trusted: an unknown required bit means
243    // the producer changed what some field *means*, so every value below is
244    // suspect, not merely incomplete.
245    if required_mask & !SUPPORTED_REQUIRED_MASK != 0 {
246        return Err(BlobError::UnsupportedRequirement(required_mask));
247    }
248    let size = rd_u64(buf, 0);
249    if size < HEADER_LEN as u64 {
250        return Err(BlobError::ForgedLength {
251            field: "size",
252            value: size,
253        });
254    }
255    // `size` may legitimately exceed `buf.len()` only if the caller handed us a
256    // short read; treat that as truncation rather than trusting the claim.
257    if size > buf.len() as u64 {
258        return Err(BlobError::Truncated {
259            need: size as usize,
260            have: buf.len(),
261        });
262    }
263    // A non-zero reserved word means a producer used it for something this
264    // build cannot see. Same reasoning as `required_mask`: refuse, do not guess.
265    if rd_u32(buf, 68) != 0 {
266        return Err(BlobError::Malformed("reserved header word is not zero"));
267    }
268    Ok(BlobHeader {
269        size,
270        required_mask,
271        planes_bytes: rd_u64(buf, 16),
272        storage_kind: rd_u32(buf, 24),
273        pid: rd_u32(buf, 28),
274        fence_fd: rd_u32(buf, 32) as i32,
275        dtype: rd_u32(buf, 36),
276        quant_axis: rd_u32(buf, 40) as i32,
277        ndim: rd_u32(buf, 44),
278        plane_count: rd_u32(buf, 48),
279        quant_scales_len: rd_u32(buf, 52),
280        quant_zero_points_len: rd_u32(buf, 56),
281        strings_bytes: rd_u32(buf, 60),
282        strides_len: rd_u32(buf, 64),
283    })
284}
285
286/// Byte offsets of every variable region, derived from the fixed header.
287///
288/// All offsets are absolute within the blob and already validated against the
289/// header's `size`, so a caller may slice with them directly.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub struct Regions {
292    /// `shape[ndim]: u64`.
293    pub shape: usize,
294    /// `strides[strides_len]: i64`, in bytes. Zero-width when densely packed.
295    pub strides: usize,
296    /// `quant_scales[]: f32`.
297    pub quant_scales: usize,
298    /// `quant_zero_points[]: i32`.
299    pub quant_zero_points: usize,
300    /// The five length-prefixed UTF-8 strings.
301    pub strings: usize,
302    /// `plane_count` records.
303    pub planes: usize,
304    /// One past the last byte the regions occupy.
305    pub end: usize,
306}
307
308/// Bounds-check a count against an element size, then advance an offset.
309///
310/// Every step is checked: these counts are attacker-controlled, and a wrapped
311/// multiply produces a small-looking length that passes a comparison it should
312/// fail. `field` names the header field so a refusal says which value was
313/// implausible.
314fn advance(
315    off: usize,
316    count: u32,
317    elem: usize,
318    limit: usize,
319    field: &'static str,
320) -> Result<usize, BlobError> {
321    let bytes = (count as usize)
322        .checked_mul(elem)
323        .ok_or(BlobError::ForgedLength {
324            field,
325            value: count as u64,
326        })?;
327    let next = off.checked_add(bytes).ok_or(BlobError::ForgedLength {
328        field,
329        value: count as u64,
330    })?;
331    if next > limit {
332        return Err(BlobError::ForgedLength {
333            field,
334            value: count as u64,
335        });
336    }
337    Ok(next)
338}
339
340/// Derive every variable region's offset from the fixed header.
341///
342/// This is the blob's reason for existing: because the header carries the
343/// length of every region, each offset is constant-time arithmetic rather than
344/// a sequential walk. It is also the point where attacker-controlled counts
345/// become dangerous, so every derivation is checked against `size` before it
346/// is returned.
347pub fn region_offsets(h: &BlobHeader) -> Result<Regions, BlobError> {
348    let limit = h.size as usize;
349    if limit < HEADER_LEN {
350        return Err(BlobError::ForgedLength {
351            field: "size",
352            value: h.size,
353        });
354    }
355    // Strides are all-or-nothing: `ndim` entries, or none. A partial array has
356    // no meaning, so treat any other count as forged rather than truncating.
357    if h.strides_len != 0 && h.strides_len != h.ndim {
358        return Err(BlobError::ForgedLength {
359            field: "strides_len",
360            value: h.strides_len as u64,
361        });
362    }
363    let shape = HEADER_LEN;
364    let strides = advance(shape, h.ndim, 8, limit, "ndim")?;
365    let quant_scales = advance(strides, h.strides_len, 8, limit, "strides_len")?;
366    let quant_zero_points = advance(
367        quant_scales,
368        h.quant_scales_len,
369        4,
370        limit,
371        "quant_scales_len",
372    )?;
373    let strings = advance(
374        quant_zero_points,
375        h.quant_zero_points_len,
376        4,
377        limit,
378        "quant_zero_points_len",
379    )?;
380    let planes = advance(strings, h.strings_bytes, 1, limit, "strings_bytes")?;
381    // Plane records are read as u64s. The strings region is written padded so
382    // this holds; a blob claiming otherwise would make every plane read
383    // unaligned, so refuse it rather than reading byte-at-a-time.
384    if !planes.is_multiple_of(8) {
385        return Err(BlobError::Malformed(
386            "planes region is not 8-aligned; strings_bytes must include padding",
387        ));
388    }
389    let planes_bytes = usize::try_from(h.planes_bytes).map_err(|_| BlobError::ForgedLength {
390        field: "planes_bytes",
391        value: h.planes_bytes,
392    })?;
393    let end = planes
394        .checked_add(planes_bytes)
395        .ok_or(BlobError::ForgedLength {
396            field: "planes_bytes",
397            value: h.planes_bytes,
398        })?;
399    if end > limit {
400        return Err(BlobError::ForgedLength {
401            field: "planes_bytes",
402            value: h.planes_bytes,
403        });
404    }
405    // Bound the plane count against the bytes actually present rather than
406    // against a constant: a forged count is a denial-of-service, and the real
407    // ceiling is how many minimum-size records could possibly fit.
408    let max_planes = planes_bytes / PLANE_RECORD_LEN;
409    if h.plane_count as usize > max_planes {
410        return Err(BlobError::ForgedLength {
411            field: "plane_count",
412            value: h.plane_count as u64,
413        });
414    }
415    Ok(Regions {
416        shape,
417        strides,
418        quant_scales,
419        quant_zero_points,
420        strings,
421        planes,
422        end,
423    })
424}
425
426/// The blob's five strings, borrowed from the buffer.
427///
428/// Strings rather than enum codes, matching `Tensor.msg` exactly. This
429/// permanently avoids the mirrored-enum bug class already seen in this repo,
430/// where `TensorMemory.MEM == 3` in Python collided with
431/// `HAL_TENSOR_MEMORY_PBO == 3` in C: a string cannot be silently reinterpreted
432/// by a consumer that disagrees about numbering.
433///
434/// `edgefirst-tensor` *carries* these; it never parses `"NV12"`.
435/// `edgefirst-image` interprets them.
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
437pub struct BlobStrings<'a> {
438    /// Format descriptor (`"NV12"`, `"rgb8"`, `"h264"`); `""` = not an image.
439    pub format: &'a str,
440    /// Colour primaries.
441    pub color_space: &'a str,
442    /// Transfer function.
443    pub color_transfer: &'a str,
444    /// YCbCr matrix; `""` for RGB.
445    pub color_encoding: &'a str,
446    /// `"full"` / `"limited"` / `""`.
447    pub color_range: &'a str,
448}
449
450/// Number of strings in the region. Fixed by the format, not by the header.
451const STRING_COUNT: usize = 5;
452
453/// Encoded length of a strings region, padded so the planes region that follows
454/// starts 8-aligned.
455pub fn strings_encoded_len(s: &BlobStrings<'_>) -> usize {
456    let raw: usize = STRING_COUNT * 4
457        + s.format.len()
458        + s.color_space.len()
459        + s.color_transfer.len()
460        + s.color_encoding.len()
461        + s.color_range.len();
462    raw.next_multiple_of(8)
463}
464
465/// Append the strings region to `out`, padded to an 8-byte boundary.
466///
467/// The padding is not cosmetic: plane records are read as `u64`s, so the
468/// planes region must begin 8-aligned. The quantization regions before this one
469/// are 4-byte-element arrays and can leave the offset merely 4-aligned, which
470/// makes this region the place alignment is restored.
471pub fn write_strings(s: &BlobStrings<'_>, out: &mut Vec<u8>) {
472    let start = out.len();
473    for part in [
474        s.format,
475        s.color_space,
476        s.color_transfer,
477        s.color_encoding,
478        s.color_range,
479    ] {
480        out.extend_from_slice(&(part.len() as u32).to_le_bytes());
481        out.extend_from_slice(part.as_bytes());
482    }
483    while !(out.len() - start).is_multiple_of(8) {
484        out.push(0);
485    }
486}
487
488/// Parse the five strings from a strings region.
489///
490/// `buf` is exactly the region — `blob[regions.strings..regions.planes]`.
491pub fn parse_strings(buf: &[u8]) -> Result<BlobStrings<'_>, BlobError> {
492    const NAMES: [&str; STRING_COUNT] = [
493        "format",
494        "color_space",
495        "color_transfer",
496        "color_encoding",
497        "color_range",
498    ];
499    let mut out: [&str; STRING_COUNT] = [""; STRING_COUNT];
500    let mut off = 0usize;
501    for (i, name) in NAMES.iter().enumerate() {
502        let name = *name;
503        let end = off
504            .checked_add(4)
505            .ok_or(BlobError::Malformed("string count overflows"))?;
506        if end > buf.len() {
507            return Err(BlobError::Truncated {
508                need: end,
509                have: buf.len(),
510            });
511        }
512        let len = rd_u32(buf, off) as usize;
513        off = end;
514        let stop = off.checked_add(len).ok_or(BlobError::ForgedLength {
515            field: name,
516            value: len as u64,
517        })?;
518        if stop > buf.len() {
519            return Err(BlobError::ForgedLength {
520                field: name,
521                value: len as u64,
522            });
523        }
524        // `from_utf8`, never `from_utf8_lossy`: a silent replacement character
525        // on a hostile buffer hides the malformed input instead of reporting it.
526        out[i] = std::str::from_utf8(&buf[off..stop]).map_err(|_| BlobError::InvalidUtf8(name))?;
527        off = stop;
528    }
529    Ok(BlobStrings {
530        format: out[0],
531        color_space: out[1],
532        color_transfer: out[2],
533        color_encoding: out[3],
534        color_range: out[4],
535    })
536}
537
538/// One plane record, borrowed from the buffer.
539///
540/// Field names and widths mirror `TensorPlane.msg` so the two map 1:1. All
541/// scalars are 64-bit, and `handle` is `i64` rather than an fd-sized `i32`:
542/// cheap now, and the alternative breaks the round-trip property, since a
543/// `u32` `size` would cap a plane at 4 GiB — which camera frames never reach
544/// but a tensor carrying model weights plausibly does.
545///
546/// The two transport modes are distinguished by `handle`:
547///
548/// * `handle >= 0` — **reference**: the bytes live behind the handle, and
549///   `data` is empty.
550/// * `handle == -1` — **inline**: the bytes are in `data`, `offset` is ignored,
551///   `size` describes `data`, and `modifier` must be 0.
552#[derive(Debug, Clone, Copy, PartialEq, Eq)]
553pub struct BlobPlane<'a> {
554    /// **In reference mode, an INDEX into the accompanying fd table** — not a
555    /// raw file descriptor. `-1` when inline.
556    ///
557    /// This is an ABI decision, not an implementation detail. A raw fd is
558    /// meaningless in the receiving process: fds travel out of band via
559    /// `SCM_RIGHTS` or `pidfd_getfd`, and the receiver is handed different
560    /// numbers than the sender used. Wayland, D-Bus and `CameraFrame` all carry
561    /// the same (payload, fds) split for the same reason.
562    ///
563    /// Indices also let planes share a descriptor: NV12's luma and chroma
564    /// commonly live in one dma-buf at different offsets, and both planes then
565    /// carry index 0 while the table holds a single fd.
566    pub handle: i64,
567    /// Byte offset of this plane within the handle; ignored when inline.
568    pub offset: u64,
569    /// Bytes per line of this plane.
570    pub stride: u64,
571    /// Plane capacity in bytes.
572    pub size: u64,
573    /// Valid payload bytes; `used <= size`.
574    pub used: u64,
575    /// DRM format modifier. 0 = linear. A consumer that does not recognise a
576    /// non-zero value must refuse, not read the plane as linear.
577    pub modifier: u64,
578    /// Opaque handle for backends whose handle is not an integer (a
579    /// `cudaIpcMemHandle_t` is 64 bytes). Empty for every storage kind today.
580    pub handle_bytes: &'a [u8],
581    /// Inlined plane bytes; non-empty only when `handle == -1`.
582    pub data: &'a [u8],
583}
584
585impl BlobPlane<'_> {
586    /// True when this plane carries its bytes rather than a reference to them.
587    pub fn is_inline(&self) -> bool {
588        self.handle < 0
589    }
590
591    /// Encoded length of this record, including the padding that keeps the
592    /// following record 8-aligned.
593    pub fn encoded_len(&self) -> usize {
594        (PLANE_RECORD_LEN + self.handle_bytes.len() + self.data.len()).next_multiple_of(8)
595    }
596}
597
598/// Append one plane record to `out`, padded so the next record starts 8-aligned.
599pub fn write_plane(p: &BlobPlane<'_>, out: &mut Vec<u8>) {
600    let start = out.len();
601    out.extend_from_slice(&p.handle.to_le_bytes());
602    out.extend_from_slice(&p.offset.to_le_bytes());
603    out.extend_from_slice(&p.stride.to_le_bytes());
604    out.extend_from_slice(&p.size.to_le_bytes());
605    out.extend_from_slice(&p.used.to_le_bytes());
606    out.extend_from_slice(&p.modifier.to_le_bytes());
607    out.extend_from_slice(&(p.handle_bytes.len() as u32).to_le_bytes());
608    out.extend_from_slice(&(p.data.len() as u32).to_le_bytes());
609    out.extend_from_slice(p.handle_bytes);
610    out.extend_from_slice(p.data);
611    while !(out.len() - start).is_multiple_of(8) {
612        out.push(0);
613    }
614}
615
616/// Read a little-endian `i64` at `off`. The caller has already bounds-checked.
617fn rd_i64(buf: &[u8], off: usize) -> i64 {
618    let mut b = [0u8; 8];
619    b.copy_from_slice(&buf[off..off + 8]);
620    i64::from_le_bytes(b)
621}
622
623/// Parse `count` plane records from a planes region.
624///
625/// `buf` is exactly the region — `blob[regions.planes..regions.end]`. `count`
626/// has already been bounded by [`region_offsets`] against the bytes present,
627/// but every per-record length is re-checked here because those live inside
628/// the region rather than in the header.
629pub fn parse_planes(buf: &[u8], count: u32) -> Result<Vec<BlobPlane<'_>>, BlobError> {
630    let mut planes = Vec::with_capacity(count as usize);
631    let mut off = 0usize;
632    for _ in 0..count {
633        let head = off
634            .checked_add(PLANE_RECORD_LEN)
635            .ok_or(BlobError::Malformed("plane record offset overflows"))?;
636        if head > buf.len() {
637            return Err(BlobError::Truncated {
638                need: head,
639                have: buf.len(),
640            });
641        }
642        let hb_len = rd_u32(buf, off + 48) as usize;
643        let data_len = rd_u32(buf, off + 52) as usize;
644        let hb_end = head.checked_add(hb_len).ok_or(BlobError::ForgedLength {
645            field: "handle_bytes_len",
646            value: hb_len as u64,
647        })?;
648        let data_end = hb_end
649            .checked_add(data_len)
650            .ok_or(BlobError::ForgedLength {
651                field: "data_len",
652                value: data_len as u64,
653            })?;
654        if hb_end > buf.len() {
655            return Err(BlobError::ForgedLength {
656                field: "handle_bytes_len",
657                value: hb_len as u64,
658            });
659        }
660        if data_end > buf.len() {
661            return Err(BlobError::ForgedLength {
662                field: "data_len",
663                value: data_len as u64,
664            });
665        }
666        planes.push(BlobPlane {
667            handle: rd_i64(buf, off),
668            offset: rd_u64(buf, off + 8),
669            stride: rd_u64(buf, off + 16),
670            size: rd_u64(buf, off + 24),
671            used: rd_u64(buf, off + 32),
672            modifier: rd_u64(buf, off + 40),
673            handle_bytes: &buf[head..hb_end],
674            data: &buf[hb_end..data_end],
675        });
676        off = data_end.next_multiple_of(8);
677    }
678    Ok(planes)
679}
680
681/// How a plane's bytes cross a boundary.
682///
683/// Both modes are first-class; the choice is the application's, not the HAL's.
684/// The HAL's job is to make it *expressible*, which is why `handle == -1` plus
685/// carried bytes is part of the format rather than a convention layered on top.
686#[derive(Debug, Clone, Copy, PartialEq, Eq)]
687pub enum TransportMode {
688    /// Carry handles. Requires the same host and an IPC-capable storage kind.
689    /// Export borrows: the handles are valid only while the source tensor lives.
690    Reference,
691    /// Carry the bytes. Always available, and *required* for `mem` and `pbo`
692    /// (which have no shareable handle) and for any storage kind travelling
693    /// over a network, where an fd and a pid are meaningless.
694    Inline,
695}
696
697/// A parsed, validated view over a blob.
698///
699/// Borrows the buffer; every accessor is a constant-time slice, not a walk.
700#[derive(Debug, Clone, Copy)]
701pub struct BlobView<'a> {
702    buf: &'a [u8],
703    header: BlobHeader,
704    regions: Regions,
705}
706
707impl<'a> BlobView<'a> {
708    /// Parse and validate a blob.
709    ///
710    /// `buf` need not start at offset 0 of its allocation — a `CameraFrame`
711    /// embeds a `Tensor` 8-aligned so a consumer can subslice and parse in
712    /// place, so this takes a slice and never requires ownership.
713    pub fn parse(buf: &'a [u8]) -> Result<Self, BlobError> {
714        let header = parse_header(buf)?;
715        let regions = region_offsets(&header)?;
716        Ok(Self {
717            buf,
718            header,
719            regions,
720        })
721    }
722
723    /// The validated fixed header.
724    pub fn header(&self) -> BlobHeader {
725        self.header
726    }
727
728    /// The addressing grid.
729    pub fn shape(&self) -> Vec<u64> {
730        (0..self.header.ndim as usize)
731            .map(|i| rd_u64(self.buf, self.regions.shape + i * 8))
732            .collect()
733    }
734
735    /// Strides in bytes; empty means densely packed C-order.
736    pub fn strides(&self) -> Vec<i64> {
737        (0..self.header.strides_len as usize)
738            .map(|i| rd_i64(self.buf, self.regions.strides + i * 8))
739            .collect()
740    }
741
742    /// The five carried strings.
743    pub fn strings(&self) -> BlobStrings<'a> {
744        parse_strings(&self.buf[self.regions.strings..self.regions.planes]).unwrap_or_default()
745    }
746
747    /// The plane records.
748    pub fn planes(&self) -> Result<Vec<BlobPlane<'a>>, BlobError> {
749        parse_planes(
750            &self.buf[self.regions.planes..self.regions.end],
751            self.header.plane_count,
752        )
753    }
754}
755
756/// Serialize a tensor to a blob in the requested transport mode.
757///
758/// Returns the blob **and its fd table**. The two travel together and are
759/// meaningless apart: a plane's `handle` is an index into the table, because a
760/// raw fd number does not survive a process boundary.
761///
762/// **Export borrows and performs no syscalls.** In [`TransportMode::Reference`]
763/// the fds in the table are the source tensor's own, valid only while it lives;
764/// `dup` happens on import, never here. That is what makes export cheap and
765/// removes any keepalive protocol between producer and consumer.
766pub fn export(t: &crate::TensorDyn, mode: TransportMode) -> crate::Result<(Vec<u8>, Vec<BlobFd>)> {
767    let fmt = t.format();
768    // The addressing grid, not the allocation: for a subsampled format these
769    // differ, and the blob carries the grid because extent comes from planes.
770    let shape = export_addressing_shape(t, fmt)?;
771    let esz = t.dtype().size() as i64;
772    let strides = export_c_strides(t, fmt, &shape, esz);
773
774    let quant = t.quantization();
775    let (quant_axis, scales, zeros): (i32, &[f32], &[i32]) = match quant {
776        None => (-2, &[], &[]),
777        Some(q) => (
778            q.axis().map(|a| a as i32).unwrap_or(-1),
779            q.scale(),
780            q.zero_point().unwrap_or(&[]),
781        ),
782    };
783
784    let colorimetry = t.colorimetry().unwrap_or_default();
785    let strings = BlobStrings {
786        format: fmt.map(|f| f.as_str()).unwrap_or(""),
787        color_space: colorimetry.space.map(|v| v.as_str()).unwrap_or(""),
788        color_transfer: colorimetry.transfer.map(|v| v.as_str()).unwrap_or(""),
789        color_encoding: colorimetry.encoding.map(|v| v.as_str()).unwrap_or(""),
790        color_range: colorimetry.range.map(|v| v.as_str()).unwrap_or(""),
791    };
792
793    // Plane geometry: from the format's table when this is an image, otherwise
794    // a single plane spanning the whole allocation.
795    let geoms = export_plane_geoms(t, fmt, esz)?;
796
797    // Bytes, only when inlining. `pin_host` is the one read here; reference
798    // mode performs no syscalls at all.
799    let pin = match mode {
800        TransportMode::Inline => Some(t.pin_host(crate::CpuAccess::Read)?),
801        TransportMode::Reference => None,
802    };
803    let all_bytes: &[u8] = match &pin {
804        Some(p) => unsafe { std::slice::from_raw_parts(p.as_mut_ptr(), p.len()) },
805        None => &[],
806    };
807
808    // The fd table. Planes sharing one descriptor (NV12 luma + chroma in a
809    // single dma-buf) must share one entry, or the receiver would dup the same
810    // buffer twice and the offsets would no longer refer to one allocation.
811    let mut fds: Vec<BlobFd> = Vec::new();
812    let handle: i64 = match mode {
813        TransportMode::Inline => -1,
814        TransportMode::Reference => {
815            let raw = reference_handle(t)?;
816            let raw = i32::try_from(raw).map_err(|_| {
817                crate::Error::InvalidOperation("handle is not representable as an fd".into())
818            })?;
819            fds.push(raw);
820            0 // index into `fds`, never the fd itself
821        }
822    };
823
824    let mut planes_buf = Vec::new();
825    write_export_planes(&geoms, mode, handle, all_bytes, &mut planes_buf)?;
826
827    let strings_bytes = strings_encoded_len(&strings);
828    let header = BlobHeader {
829        size: 0, // patched below
830        required_mask: 0,
831        planes_bytes: planes_buf.len() as u64,
832        storage_kind: t.memory().code(),
833        // No handle to reopen once the bytes travel, so no pid to reopen it with.
834        pid: match mode {
835            TransportMode::Inline => 0,
836            TransportMode::Reference => std::process::id(),
837        },
838        // Reserved in this build, and meaningless inline in any case: inlining
839        // means the producer already read the bytes, so it already waited.
840        fence_fd: -1,
841        dtype: t.dtype().code(),
842        quant_axis,
843        ndim: shape.len() as u32,
844        plane_count: geoms.len() as u32,
845        quant_scales_len: scales.len() as u32,
846        quant_zero_points_len: zeros.len() as u32,
847        strings_bytes: strings_bytes as u32,
848        strides_len: strides.len() as u32,
849    };
850
851    let mut out = vec![0u8; HEADER_LEN];
852    for d in &shape {
853        out.extend_from_slice(&d.to_le_bytes());
854    }
855    for st in &strides {
856        out.extend_from_slice(&st.to_le_bytes());
857    }
858    for sc in scales {
859        out.extend_from_slice(&sc.to_le_bytes());
860    }
861    for z in zeros {
862        out.extend_from_slice(&z.to_le_bytes());
863    }
864    write_strings(&strings, &mut out);
865    out.extend_from_slice(&planes_buf);
866
867    let mut header = header;
868    header.size = out.len() as u64;
869    header.write_to(&mut out);
870    Ok((out, fds))
871}
872
873fn export_addressing_shape(
874    t: &crate::TensorDyn,
875    fmt: Option<crate::PixelFormat>,
876) -> crate::Result<Vec<u64>> {
877    match fmt {
878        Some(f) => {
879            let (w, h) = image_dims(t, f)?;
880            Ok(f.addressing_shape(w, h)
881                .ok_or_else(|| {
882                    crate::Error::InvalidArgument(format!("no addressing shape for {f:?}"))
883                })?
884                .iter()
885                .map(|d| *d as u64)
886                .collect())
887        }
888        None => Ok(t.shape().iter().map(|d| *d as u64).collect()),
889    }
890}
891
892fn export_c_strides(
893    t: &crate::TensorDyn,
894    fmt: Option<crate::PixelFormat>,
895    shape: &[u64],
896    esz: i64,
897) -> Vec<i64> {
898    let mut acc = esz;
899    let mut v = vec![0i64; shape.len()];
900    for i in (0..shape.len()).rev() {
901        v[i] = acc;
902        acc *= shape[i] as i64;
903    }
904    if let (Some(rs), true) = (t.row_stride(), shape.len() >= 2) {
905        let row_dim = match fmt.map(|f| f.layout()) {
906            Some(crate::PixelLayout::Planar) if shape.len() >= 3 => 1,
907            _ => 0,
908        };
909        v[row_dim] = rs as i64;
910    }
911    v
912}
913
914fn export_plane_geoms(
915    t: &crate::TensorDyn,
916    fmt: Option<crate::PixelFormat>,
917    esz: i64,
918) -> crate::Result<Vec<crate::PlaneGeometry>> {
919    match fmt {
920        Some(f) => {
921            let (w, h) = image_dims(t, f)?;
922            let rs = t
923                .effective_row_stride()
924                .unwrap_or(w * f.channels() * esz as usize);
925            f.plane_table(w, h, rs)
926                .ok_or_else(|| crate::Error::InvalidArgument(format!("no plane table for {f:?}")))
927        }
928        None => Ok(vec![crate::PlaneGeometry {
929            offset: 0,
930            stride: t.capacity_bytes() as u64,
931            size: t.capacity_bytes() as u64,
932        }]),
933    }
934}
935
936fn write_export_planes(
937    geoms: &[crate::PlaneGeometry],
938    mode: TransportMode,
939    handle: i64,
940    all_bytes: &[u8],
941    planes_buf: &mut Vec<u8>,
942) -> crate::Result<()> {
943    for g in geoms {
944        let data: &[u8] = match mode {
945            TransportMode::Inline => {
946                let start = g.offset as usize;
947                let end = start
948                    .checked_add(g.size as usize)
949                    .ok_or(crate::Error::InvalidSize(g.size as usize))?;
950                all_bytes
951                    .get(start..end)
952                    .ok_or(crate::Error::InvalidSize(end))?
953            }
954            TransportMode::Reference => &[],
955        };
956        write_plane(
957            &BlobPlane {
958                handle,
959                // Ignored when inline, and zeroed so the record cannot be
960                // misread as pointing into a handle it does not have.
961                offset: if mode == TransportMode::Inline {
962                    0
963                } else {
964                    g.offset
965                },
966                stride: g.stride,
967                // Inline: `size` describes `data`, per the schema's own rule.
968                size: if mode == TransportMode::Inline {
969                    data.len() as u64
970                } else {
971                    g.size
972                },
973                used: if mode == TransportMode::Inline {
974                    data.len() as u64
975                } else {
976                    g.size
977                },
978                modifier: 0,
979                handle_bytes: &[],
980                data,
981            },
982            planes_buf,
983        );
984    }
985    Ok(())
986}
987
988/// Recover an image's logical width and height from its allocation shape.
989///
990/// The tensor stores its allocation geometry; the blob needs the logical
991/// dimensions to ask the format for a grid and a plane table.
992fn image_dims(t: &crate::TensorDyn, f: crate::PixelFormat) -> crate::Result<(usize, usize)> {
993    let shape = t.shape();
994    let bad = || crate::Error::InvalidShape(format!("shape {shape:?} is not an image for {f:?}"));
995    match f.layout() {
996        crate::PixelLayout::Packed => {
997            if shape.len() < 2 {
998                return Err(bad());
999            }
1000            Ok((shape[1], shape[0]))
1001        }
1002        crate::PixelLayout::Planar => {
1003            if shape.len() < 3 {
1004                return Err(bad());
1005            }
1006            Ok((shape[2], shape[1]))
1007        }
1008        crate::PixelLayout::SemiPlanar => {
1009            if shape.len() < 2 {
1010                return Err(bad());
1011            }
1012            // The stored shape is the combined-plane allocation; invert it to
1013            // recover the luma height rather than assuming h*2/3, which is
1014            // wrong for odd heights and for NV16/NV24.
1015            let total = shape[0];
1016            let h = (0..=total)
1017                .find(|&h| f.combined_plane_height(h) == Some(total))
1018                .ok_or_else(bad)?;
1019            Ok((shape[1], h))
1020        }
1021    }
1022}
1023
1024/// The handle a reference-mode export borrows, or an error when the storage
1025/// kind has none to share.
1026fn reference_handle(t: &crate::TensorDyn) -> crate::Result<i64> {
1027    match t.memory() {
1028        #[cfg(target_os = "linux")]
1029        crate::TensorMemory::DmaBuf => {
1030            use std::os::fd::AsRawFd;
1031            // `dmabuf()` is fallible rather than optional on Linux, so this
1032            // maps the error instead of an absent value.
1033            t.dmabuf().map(|fd| fd.as_raw_fd() as i64).map_err(|e| {
1034                crate::Error::InvalidOperation(format!("dma-buf tensor has no fd: {e}"))
1035            })
1036        }
1037        #[cfg(any(target_os = "macos", target_os = "ios"))]
1038        crate::TensorMemory::DmaBuf => t
1039            .iosurface_id()
1040            .map(|id| id as i64)
1041            .ok_or_else(|| crate::Error::InvalidOperation("IOSurface tensor has no id".into())),
1042        // `mem` and `pbo` have no shareable handle at all. Refusing beats
1043        // silently inlining: the caller asked for a reference and would
1044        // otherwise receive a copy without being told.
1045        other => Err(crate::Error::InvalidOperation(format!(
1046            "{other:?} has no shareable handle; export it with TransportMode::Inline"
1047        ))),
1048    }
1049}
1050
1051/// Reconstruct a tensor from a blob.
1052///
1053/// **Import dups every fd it retains**, so the result is fully independent and
1054/// the source tensor may die immediately. In inline mode the result is a new
1055/// allocation holding a copy of the carried bytes.
1056///
1057/// `fds` is the table that travelled beside the blob; a reference-mode plane's
1058/// `handle` indexes into it. Pass an empty slice for an inline blob.
1059///
1060/// `blob` is untrusted: it may come from another wheel, another process, or a
1061/// network. Every length has already been bounded by [`BlobView::parse`]; this
1062/// function adds the *semantic* checks that parsing alone cannot make, and
1063/// every index is bounded against `fds` before use.
1064pub fn import(blob: &[u8], fds: &[BlobFd]) -> crate::Result<crate::TensorDyn> {
1065    #[cfg(not(unix))]
1066    let _ = fds;
1067    let v = BlobView::parse(blob).map_err(|e| crate::Error::InvalidArgument(e.to_string()))?;
1068    let h = v.header();
1069    let planes = v
1070        .planes()
1071        .map_err(|e| crate::Error::InvalidArgument(e.to_string()))?;
1072
1073    // All planes inline, or none. A frame mixing transport modes has no
1074    // coherent meaning: one storage_kind, pid and fence_fd cover every plane,
1075    // so half of them cannot be somewhere else.
1076    let inline = planes.first().map(BlobPlane::is_inline).unwrap_or(true);
1077    validate_import_planes(&planes, inline)?;
1078
1079    let dtype = crate::protocol::dtype_to_dtype(h.dtype)
1080        .ok_or_else(|| crate::Error::NotImplemented(format!("blob dtype code {}", h.dtype)))?;
1081    let strings = v.strings();
1082    let format = if strings.format.is_empty() {
1083        None
1084    } else {
1085        Some(
1086            crate::PixelFormat::from_str_code(strings.format).ok_or_else(|| {
1087                crate::Error::NotImplemented(format!("blob format {:?}", strings.format))
1088            })?,
1089        )
1090    };
1091
1092    if !inline {
1093        #[cfg(not(unix))]
1094        {
1095            return Err(crate::Error::NotImplemented(
1096                "reference-mode blob import is Unix-only".into(),
1097            ));
1098        }
1099        #[cfg(unix)]
1100        {
1101            return import_referenced_blob(&v, &h, &planes, fds, format, &strings, dtype);
1102        }
1103    }
1104
1105    // Allocate the ALLOCATION geometry, not the grid. The grid is what the blob
1106    // carries; the buffer that backs it is larger for a subsampled format, and
1107    // sizing from the grid would undersize NV12 by a third.
1108    let grid: Vec<usize> = v.shape().iter().map(|d| *d as usize).collect();
1109    let alloc_shape = alloc_shape_from_grid(format, &grid)?;
1110
1111    let mut t = crate::TensorDyn::new(&alloc_shape, dtype, Some(crate::TensorMemory::Mem), None)?;
1112    if let Some(f) = format {
1113        t.set_format(f)?;
1114    }
1115
1116    // Copy each plane back to where the format says it belongs. The exporter
1117    // zeroes `offset` on an inline plane (it names a position inside a handle
1118    // that is not being carried), so the layout is rebuilt from the plane
1119    // table rather than trusted from the wire.
1120    blit_inline_planes(&mut t, &planes, format, &grid, dtype)?;
1121
1122    t.set_colorimetry(colorimetry_from(&strings));
1123    if h.quant_axis != -2 {
1124        let scales: Vec<f32> = (0..h.quant_scales_len as usize)
1125            .map(|i| {
1126                let mut b = [0u8; 4];
1127                let off = v.regions.quant_scales + i * 4;
1128                b.copy_from_slice(&blob[off..off + 4]);
1129                f32::from_le_bytes(b)
1130            })
1131            .collect();
1132        let zeros: Vec<i32> = (0..h.quant_zero_points_len as usize)
1133            .map(|i| {
1134                let off = v.regions.quant_zero_points + i * 4;
1135                rd_u32(blob, off) as i32
1136            })
1137            .collect();
1138        if let Some(q) = quantization_from(h.quant_axis, &scales, &zeros) {
1139            t.set_quantization(q)?;
1140        }
1141    }
1142    Ok(t)
1143}
1144
1145fn validate_import_planes(planes: &[BlobPlane<'_>], inline: bool) -> crate::Result<()> {
1146    if planes.iter().any(|p| p.is_inline() != inline) {
1147        return Err(crate::Error::InvalidArgument(
1148            "blob mixes inline and referenced planes; a tensor has one transport mode".into(),
1149        ));
1150    }
1151    for p in planes {
1152        if p.is_inline() {
1153            // The schemas validator's own rules, enforced on the way in too.
1154            if p.size as usize != p.data.len() {
1155                return Err(crate::Error::InvalidArgument(format!(
1156                    "inline plane declares size {} but carries {} bytes",
1157                    p.size,
1158                    p.data.len()
1159                )));
1160            }
1161            if p.modifier != 0 || !p.handle_bytes.is_empty() {
1162                return Err(crate::Error::InvalidArgument(
1163                    "inline plane must have modifier 0 and no handle_bytes".into(),
1164                ));
1165            }
1166        } else if !p.data.is_empty() {
1167            return Err(crate::Error::InvalidArgument(
1168                "referenced plane must not also carry inline bytes".into(),
1169            ));
1170        }
1171    }
1172    Ok(())
1173}
1174
1175fn alloc_shape_from_grid(
1176    format: Option<crate::PixelFormat>,
1177    grid: &[usize],
1178) -> crate::Result<Vec<usize>> {
1179    match format {
1180        Some(f) => {
1181            let (w, h_px) = dims_from_grid(f, grid)?;
1182            f.allocation_shape(w, h_px)
1183                .ok_or_else(|| crate::Error::InvalidShape(format!("no allocation shape for {f:?}")))
1184        }
1185        None => Ok(grid.to_vec()),
1186    }
1187}
1188
1189#[cfg(unix)]
1190fn import_referenced_blob(
1191    v: &BlobView<'_>,
1192    h: &BlobHeader,
1193    planes: &[BlobPlane<'_>],
1194    fds: &[BlobFd],
1195    format: Option<crate::PixelFormat>,
1196    strings: &BlobStrings<'_>,
1197    dtype: crate::DType,
1198) -> crate::Result<crate::TensorDyn> {
1199    // Reference mode. Every plane's `handle` is an index into `fds`, and
1200    // the index is bounded before use: it arrived from an untrusted blob,
1201    // and an out-of-range read here would hand an arbitrary descriptor to
1202    // `dup`.
1203    let first = planes
1204        .first()
1205        .ok_or_else(|| crate::Error::InvalidArgument("referenced blob carries no planes".into()))?;
1206    let idx = usize::try_from(first.handle).map_err(|_| {
1207        crate::Error::InvalidArgument(format!("plane handle {} is not an index", first.handle))
1208    })?;
1209    let raw = *fds.get(idx).ok_or_else(|| {
1210        crate::Error::InvalidArgument(format!(
1211            "plane handle indexes fd {idx} but only {} were provided",
1212            fds.len()
1213        ))
1214    })?;
1215    for p in planes {
1216        let i = usize::try_from(p.handle).unwrap_or(usize::MAX);
1217        if i >= fds.len() {
1218            return Err(crate::Error::InvalidArgument(format!(
1219                "plane handle indexes fd {i} but only {} were provided",
1220                fds.len()
1221            )));
1222        }
1223    }
1224
1225    let grid: Vec<usize> = v.shape().iter().map(|d| *d as usize).collect();
1226    let alloc_shape = alloc_shape_from_grid(format, &grid)?;
1227    let mut t = open_referenced_tensor(h.storage_kind, raw, &alloc_shape, dtype)?;
1228    if let Some(f) = format {
1229        t.set_format(f)?;
1230    }
1231    if first.stride > 0 {
1232        // The producer's pitch, which the shape alone cannot express.
1233        let _ = t.set_row_stride(first.stride as usize);
1234    }
1235    t.set_colorimetry(colorimetry_from(strings));
1236    Ok(t)
1237}
1238
1239#[cfg(unix)]
1240fn open_referenced_tensor(
1241    storage_kind: u32,
1242    raw: BlobFd,
1243    alloc_shape: &[usize],
1244    dtype: crate::DType,
1245) -> crate::Result<crate::TensorDyn> {
1246    // The table entry is a PLATFORM handle, not universally a file
1247    // descriptor: on Linux it is a dma-buf fd, on macOS an IOSurface ID.
1248    // Dispatching on `storage_kind` rather than assuming an fd is required
1249    // -- treating an IOSurface ID as an fd yields EBADF, which is how this
1250    // gap was found.
1251    match crate::TensorMemory::from_code(storage_kind) {
1252        #[cfg(any(target_os = "macos", target_os = "ios"))]
1253        Some(crate::TensorMemory::DmaBuf) | Some(crate::TensorMemory::IoSurface) => {
1254            let id = u32::try_from(raw).map_err(|_| {
1255                crate::Error::InvalidArgument(format!("{raw} is not an IOSurface id"))
1256            })?;
1257            crate::TensorDyn::from_iosurface_id(id, alloc_shape, dtype, None)
1258        }
1259        #[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
1260        Some(crate::TensorMemory::DmaBuf) | Some(crate::TensorMemory::Shm) => {
1261            use std::os::fd::{BorrowedFd, OwnedFd};
1262            // Import dups: the result is independent and the producer may
1263            // die immediately, which is what removes the keepalive protocol.
1264            // SAFETY: `raw` is live for this call -- the caller owns it and
1265            // has not yet closed its own copy. Borrow, never adopt.
1266            let borrowed = unsafe { BorrowedFd::borrow_raw(raw) };
1267            let owned: OwnedFd = borrowed
1268                .try_clone_to_owned()
1269                .map_err(crate::Error::IoError)?;
1270            crate::TensorDyn::from_fd(owned, alloc_shape, dtype, None)
1271        }
1272        other => Err(crate::Error::NotImplemented(format!(
1273            "reference-mode import for {other:?} on this platform"
1274        ))),
1275    }
1276}
1277
1278fn blit_inline_planes(
1279    t: &mut crate::TensorDyn,
1280    planes: &[BlobPlane<'_>],
1281    format: Option<crate::PixelFormat>,
1282    grid: &[usize],
1283    dtype: crate::DType,
1284) -> crate::Result<()> {
1285    let pin = t.pin_host(crate::CpuAccess::Write)?;
1286    let dst = unsafe { std::slice::from_raw_parts_mut(pin.as_mut_ptr(), pin.len()) };
1287    let geoms: Vec<crate::PlaneGeometry> = match format {
1288        Some(f) => {
1289            let (w, h_px) = dims_from_grid(f, grid)?;
1290            let rs = t
1291                .effective_row_stride()
1292                .unwrap_or(w * f.channels() * dtype.size());
1293            f.plane_table(w, h_px, rs)
1294                .ok_or_else(|| crate::Error::InvalidShape(format!("no plane table for {f:?}")))?
1295        }
1296        None => vec![crate::PlaneGeometry {
1297            offset: 0,
1298            stride: dst.len() as u64,
1299            size: dst.len() as u64,
1300        }],
1301    };
1302    if geoms.len() != planes.len() {
1303        return Err(crate::Error::InvalidArgument(format!(
1304            "blob carries {} planes but {:?} has {}",
1305            planes.len(),
1306            format,
1307            geoms.len()
1308        )));
1309    }
1310    for (g, p) in geoms.iter().zip(planes) {
1311        let start = g.offset as usize;
1312        let end = start
1313            .checked_add(p.data.len())
1314            .ok_or(crate::Error::InvalidSize(p.data.len()))?;
1315        let slot = dst
1316            .get_mut(start..end)
1317            .ok_or(crate::Error::InvalidSize(end))?;
1318        slot.copy_from_slice(p.data);
1319    }
1320    Ok(())
1321}
1322
1323/// Rebuild `Colorimetry` from the carried strings. An unrecognised value is
1324/// dropped to `None` rather than refused: colorimetry is descriptive metadata,
1325/// and a consumer that does not know one axis can still use the pixels.
1326fn colorimetry_from(s: &BlobStrings<'_>) -> Option<crate::Colorimetry> {
1327    let c = crate::Colorimetry {
1328        space: crate::ColorSpace::from_str_code(s.color_space),
1329        transfer: crate::ColorTransfer::from_str_code(s.color_transfer),
1330        encoding: crate::ColorEncoding::from_str_code(s.color_encoding),
1331        range: crate::ColorRange::from_str_code(s.color_range),
1332    };
1333    if c == crate::Colorimetry::default() {
1334        None
1335    } else {
1336        Some(c)
1337    }
1338}
1339
1340/// Rebuild a `Quantization` from the wire representation.
1341fn quantization_from(axis: i32, scales: &[f32], zeros: &[i32]) -> Option<crate::Quantization> {
1342    let first = *scales.first()?;
1343    match (axis, zeros.first()) {
1344        (-1, None) => Some(crate::Quantization::per_tensor_symmetric(first)),
1345        (-1, Some(z)) => Some(crate::Quantization::per_tensor(first, *z)),
1346        (a, _) if a >= 0 => {
1347            crate::Quantization::per_channel_symmetric(scales.to_vec(), a as usize).ok()
1348        }
1349        _ => None,
1350    }
1351}
1352
1353/// Recover logical width and height from an addressing grid.
1354///
1355/// The inverse of [`PixelFormat::addressing_shape`](crate::PixelFormat::addressing_shape).
1356fn dims_from_grid(f: crate::PixelFormat, grid: &[usize]) -> crate::Result<(usize, usize)> {
1357    let bad = || crate::Error::InvalidShape(format!("grid {grid:?} is not an image for {f:?}"));
1358    match f.layout() {
1359        crate::PixelLayout::Planar => {
1360            if grid.len() < 3 {
1361                return Err(bad());
1362            }
1363            Ok((grid[2], grid[1]))
1364        }
1365        // Packed multi-channel is [h, w, c]; single-channel packed and
1366        // semi-planar are both [h, w].
1367        _ => {
1368            if grid.len() < 2 {
1369                return Err(bad());
1370            }
1371            Ok((grid[1], grid[0]))
1372        }
1373    }
1374}
1375
1376#[cfg(test)]
1377mod tests {
1378    use super::*;
1379
1380    #[test]
1381    fn every_region_offset_is_constant_time_arithmetic() {
1382        // The property that distinguishes the blob from CDR: no sequential walk.
1383        let h = BlobHeader {
1384            size: 4096,
1385            ndim: 3,
1386            strides_len: 3,
1387            quant_scales_len: 4,
1388            quant_zero_points_len: 4,
1389            strings_bytes: 24,
1390            plane_count: 2,
1391            planes_bytes: 128,
1392            ..BlobHeader::empty()
1393        };
1394        let r = region_offsets(&h).unwrap();
1395        assert_eq!(r.shape, HEADER_LEN);
1396        assert_eq!(r.strides, HEADER_LEN + 3 * 8);
1397        assert_eq!(r.quant_scales, HEADER_LEN + 3 * 8 + 3 * 8);
1398        assert_eq!(r.quant_zero_points, r.quant_scales + 4 * 4);
1399        assert_eq!(r.strings, r.quant_zero_points + 4 * 4);
1400        assert_eq!(r.planes, r.strings + 24);
1401        assert_eq!(r.end, r.planes + 128);
1402    }
1403
1404    #[test]
1405    fn empty_strides_leave_the_region_zero_width() {
1406        let h = BlobHeader {
1407            size: 4096,
1408            ndim: 2,
1409            strides_len: 0,
1410            ..BlobHeader::empty()
1411        };
1412        let r = region_offsets(&h).unwrap();
1413        assert_eq!(r.strides, HEADER_LEN + 2 * 8);
1414        assert_eq!(
1415            r.quant_scales, r.strides,
1416            "densely-packed strides occupy no bytes"
1417        );
1418    }
1419
1420    #[test]
1421    fn strides_must_be_ndim_entries_or_none() {
1422        // A partial stride array has no meaning; it is a forged length.
1423        let h = BlobHeader {
1424            size: 4096,
1425            ndim: 3,
1426            strides_len: 2,
1427            ..BlobHeader::empty()
1428        };
1429        assert!(matches!(
1430            region_offsets(&h),
1431            Err(BlobError::ForgedLength {
1432                field: "strides_len",
1433                ..
1434            })
1435        ));
1436    }
1437
1438    #[test]
1439    fn a_forged_ndim_cannot_derive_an_offset_past_the_buffer() {
1440        let h = BlobHeader {
1441            size: 72,
1442            ndim: u32::MAX,
1443            strides_len: u32::MAX,
1444            ..BlobHeader::empty()
1445        };
1446        assert!(
1447            region_offsets(&h).is_err(),
1448            "ndim * 8 must be checked, not wrapped"
1449        );
1450    }
1451
1452    #[test]
1453    fn regions_that_overrun_size_are_refused() {
1454        // Every derived offset is validated against `size`, which parse_header
1455        // has already bounded by the real buffer length.
1456        let h = BlobHeader {
1457            size: 80,
1458            ndim: 2,
1459            strides_len: 2,
1460            plane_count: 1,
1461            planes_bytes: 4096,
1462            ..BlobHeader::empty()
1463        };
1464        assert!(region_offsets(&h).is_err());
1465    }
1466
1467    #[test]
1468    fn plane_count_is_bounded_by_the_planes_region_not_by_a_constant() {
1469        // A forged plane_count is a denial-of-service: it must be refused
1470        // against the bytes actually present, before any allocation.
1471        let h = BlobHeader {
1472            size: 4096,
1473            plane_count: u32::MAX,
1474            planes_bytes: 56,
1475            ..BlobHeader::empty()
1476        };
1477        assert!(matches!(
1478            region_offsets(&h),
1479            Err(BlobError::ForgedLength {
1480                field: "plane_count",
1481                ..
1482            })
1483        ));
1484    }
1485
1486    /// Fill a tensor's bytes with a recognisable ramp so a content comparison
1487    /// can actually fail. An all-zero buffer round-trips through almost any bug.
1488    fn fill_ramp(t: &TensorDyn) {
1489        let pin = t.pin_host(CpuAccess::ReadWrite).expect("pin");
1490        let s = unsafe { std::slice::from_raw_parts_mut(pin.as_mut_ptr(), pin.len()) };
1491        for (i, b) in s.iter_mut().enumerate() {
1492            *b = (i % 251) as u8;
1493        }
1494    }
1495
1496    fn bytes_of(t: &TensorDyn) -> Vec<u8> {
1497        let pin = t.pin_host(CpuAccess::Read).expect("pin");
1498        unsafe { std::slice::from_raw_parts(pin.as_mut_ptr(), pin.len()) }.to_vec()
1499    }
1500
1501    /// A reference-capable tensor, or `None` on a host that has no shareable
1502    /// backing (no dma-heap, or one this user cannot open).
1503    fn reference_capable(w: usize, h: usize) -> Option<TensorDyn> {
1504        match Tensor::<u8>::image(
1505            w,
1506            h,
1507            PixelFormat::Nv12,
1508            Some(TensorMemory::DmaBuf),
1509            CpuAccess::ReadWrite,
1510        ) {
1511            Ok(t) => Some(TensorDyn::from(t)),
1512            Err(e) => {
1513                use std::io::Write;
1514                let _ = writeln!(
1515                    std::io::stderr(),
1516                    "SKIP: no shareable backing on this host ({e:?}); \
1517                     reference transport not exercised"
1518                );
1519                None
1520            }
1521        }
1522    }
1523
1524    #[test]
1525    fn a_reference_export_puts_an_index_in_the_handle_not_a_raw_fd() {
1526        let Some(src) = reference_capable(64, 48) else {
1527            return;
1528        };
1529        let (blob, fds) = export(&src, TransportMode::Reference).unwrap();
1530        let v = BlobView::parse(&blob).unwrap();
1531        let planes = v.planes().unwrap();
1532        assert!(!fds.is_empty(), "reference mode must carry an fd table");
1533        for p in &planes {
1534            assert!(!p.is_inline());
1535            assert!(
1536                (p.handle as usize) < fds.len(),
1537                "handle {} must index the {}-entry fd table, not be a raw fd",
1538                p.handle,
1539                fds.len()
1540            );
1541        }
1542        // NV12's two planes share one allocation, so they share one table entry
1543        // -- duplicating it would make the receiver dup the same buffer twice
1544        // and the offsets would stop referring to one buffer.
1545        assert_eq!(fds.len(), 1, "planes sharing a buffer share a table entry");
1546        assert!(planes.iter().all(|p| p.handle == 0));
1547    }
1548
1549    #[test]
1550    fn a_reference_round_trip_preserves_content_and_the_source_may_die() {
1551        let Some(src) = reference_capable(64, 48) else {
1552            return;
1553        };
1554        fill_ramp(&src);
1555        let expected = bytes_of(&src);
1556        let (blob, fds) = export(&src, TransportMode::Reference).unwrap();
1557        let src_id = src.buffer_identity().id();
1558        let got = import(&blob, &fds).unwrap();
1559
1560        // The property that makes reference mode worth having: this is the
1561        // SAME buffer, not a copy that happens to hold equal bytes. `dup`
1562        // preserves the underlying object, so derived identity survives the
1563        // round trip -- which is also what lets the GL import cache hit on a
1564        // tensor that arrived from another process.
1565        assert_eq!(
1566            got.buffer_identity().id(),
1567            src_id,
1568            "reference mode must share the buffer, not copy it"
1569        );
1570
1571        // Import dups, so dropping the producer must not invalidate the import.
1572        drop(src);
1573        assert_eq!(
1574            bytes_of(&got),
1575            expected,
1576            "the imported tensor sees the bytes"
1577        );
1578        assert_eq!(got.format(), Some(PixelFormat::Nv12));
1579    }
1580
1581    #[test]
1582    fn an_fd_index_past_the_table_is_refused() {
1583        // The index arrives from an untrusted blob. An unbounded read here
1584        // would hand an arbitrary descriptor to dup.
1585        let Some(src) = reference_capable(64, 48) else {
1586            return;
1587        };
1588        let (blob, fds) = export(&src, TransportMode::Reference).unwrap();
1589        assert!(
1590            import(&blob, &[]).is_err(),
1591            "an empty table cannot satisfy an index"
1592        );
1593        let mut tampered = blob.clone();
1594        let r = region_offsets(&BlobView::parse(&blob).unwrap().header()).unwrap();
1595        tampered[r.planes..r.planes + 8].copy_from_slice(&99i64.to_le_bytes());
1596        assert!(
1597            import(&tampered, &fds).is_err(),
1598            "an out-of-range index must be refused, not read"
1599        );
1600    }
1601
1602    #[test]
1603    fn inline_round_trip_preserves_content_and_every_metadata_field() {
1604        let src = nv12_mem(64, 48).with_colorimetry(crate::Colorimetry::jfif());
1605        fill_ramp(&src);
1606        let blob = export(&src, TransportMode::Inline).unwrap().0;
1607        let got = import(&blob, &[]).unwrap();
1608
1609        assert_eq!(got.dtype(), src.dtype());
1610        assert_eq!(got.format(), src.format());
1611        assert_eq!(got.colorimetry(), src.colorimetry());
1612        assert_eq!(got.shape(), src.shape(), "allocation geometry is rebuilt");
1613        assert_eq!(bytes_of(&got), bytes_of(&src), "every byte survived");
1614    }
1615
1616    #[test]
1617    fn inline_import_is_a_new_allocation_and_the_source_may_die() {
1618        let src = nv12_mem(64, 48);
1619        fill_ramp(&src);
1620        let expected = bytes_of(&src);
1621        let blob = export(&src, TransportMode::Inline).unwrap().0;
1622        drop(src);
1623        let got = import(&blob, &[]).unwrap();
1624        assert_eq!(bytes_of(&got), expected);
1625    }
1626
1627    #[test]
1628    fn a_bare_tensor_round_trips_including_byte_strides() {
1629        let src =
1630            TensorDyn::from(Tensor::<f32>::new(&[3, 8], Some(TensorMemory::Mem), None).unwrap());
1631        fill_ramp(&src);
1632        let blob = export(&src, TransportMode::Inline).unwrap().0;
1633        let got = import(&blob, &[]).unwrap();
1634        assert_eq!(got.shape(), &[3, 8]);
1635        assert_eq!(got.dtype(), crate::DType::F32);
1636        assert_eq!(got.format(), None);
1637        assert_eq!(bytes_of(&got), bytes_of(&src));
1638    }
1639
1640    /// Assemble a minimal valid blob around a hand-written plane set.
1641    ///
1642    /// Tampering an exported blob is not good enough for the mode-mix test:
1643    /// flipping a plane's `handle` also leaves its bytes attached, which trips
1644    /// the separate "a referenced plane must not carry bytes" check, so the
1645    /// test would pass with the mode-mix guard removed entirely. Building the
1646    /// plane set directly isolates the one property under test.
1647    fn build_blob(planes: &[BlobPlane<'_>]) -> Vec<u8> {
1648        let strings = BlobStrings::default();
1649        let mut planes_buf = Vec::new();
1650        for p in planes {
1651            write_plane(p, &mut planes_buf);
1652        }
1653        let mut out = vec![0u8; HEADER_LEN];
1654        out.extend_from_slice(&1u64.to_le_bytes()); // shape [1]
1655        write_strings(&strings, &mut out);
1656        out.extend_from_slice(&planes_buf);
1657        let h = BlobHeader {
1658            size: out.len() as u64,
1659            planes_bytes: planes_buf.len() as u64,
1660            dtype: crate::DType::U8.code(),
1661            quant_axis: -2,
1662            ndim: 1,
1663            plane_count: planes.len() as u32,
1664            strings_bytes: strings_encoded_len(&strings) as u32,
1665            ..BlobHeader::empty()
1666        };
1667        h.write_to(&mut out);
1668        out
1669    }
1670
1671    #[test]
1672    fn no_corrupted_header_value_can_panic_the_parser() {
1673        // The blanket property behind every individual forged-field test: a
1674        // panic on hostile input is a denial-of-service, so the parser must
1675        // return Err for anything it cannot handle. Walks every 4-byte word of
1676        // the header through several hostile values rather than checking the
1677        // handful of fields someone thought to name.
1678        let good = export(&nv12_mem(64, 48), TransportMode::Inline).unwrap().0;
1679        for word in 0..HEADER_LEN / 4 {
1680            for pattern in [u32::MAX, u32::MAX / 2, 1, 0x8000_0000] {
1681                let mut bad = good.clone();
1682                bad[word * 4..word * 4 + 4].copy_from_slice(&pattern.to_le_bytes());
1683                // Either it parses (the value happened to be benign) or it
1684                // errors. It must never panic, and `import` must not either.
1685                if let Ok(v) = BlobView::parse(&bad) {
1686                    let _ = v.shape();
1687                    let _ = v.strides();
1688                    let _ = v.strings();
1689                    let _ = v.planes();
1690                }
1691                let _ = import(&bad, &[]);
1692            }
1693        }
1694    }
1695
1696    #[test]
1697    fn no_corrupted_plane_record_can_panic_the_parser() {
1698        let good = export(&nv12_mem(64, 48), TransportMode::Inline).unwrap().0;
1699        let r = region_offsets(&BlobView::parse(&good).unwrap().header()).unwrap();
1700        for word in 0..PLANE_RECORD_LEN / 4 {
1701            for pattern in [u32::MAX, u32::MAX / 2, 0x8000_0000] {
1702                let mut bad = good.clone();
1703                let at = r.planes + word * 4;
1704                bad[at..at + 4].copy_from_slice(&pattern.to_le_bytes());
1705                if let Ok(v) = BlobView::parse(&bad) {
1706                    let _ = v.planes();
1707                }
1708                let _ = import(&bad, &[]);
1709            }
1710        }
1711    }
1712
1713    #[test]
1714    fn a_mixed_inline_and_referenced_plane_set_is_refused() {
1715        // The schemas validator's rule: all planes inline, or none. A frame
1716        // mixing modes has no coherent meaning, since one storage_kind, pid
1717        // and fence_fd cover every plane.
1718        //
1719        // Both planes here are individually well-formed -- the referenced one
1720        // carries no bytes, the inline one's size matches its data -- so the
1721        // ONLY thing wrong is that they disagree about transport mode.
1722        let payload = [1u8, 2, 3, 4];
1723        let referenced = BlobPlane {
1724            handle: 7,
1725            offset: 0,
1726            stride: 4,
1727            size: 4,
1728            used: 4,
1729            modifier: 0,
1730            handle_bytes: &[],
1731            data: &[],
1732        };
1733        let inlined = BlobPlane {
1734            handle: -1,
1735            offset: 0,
1736            stride: 4,
1737            size: payload.len() as u64,
1738            used: payload.len() as u64,
1739            modifier: 0,
1740            handle_bytes: &[],
1741            data: &payload,
1742        };
1743        let blob = build_blob(&[referenced, inlined]);
1744        let err =
1745            import(&blob, &[]).expect_err("a plane set mixing transport modes must be refused");
1746        assert!(
1747            err.to_string().contains("one transport mode"),
1748            "must fail on the mode mix, not incidentally: {err}"
1749        );
1750    }
1751
1752    #[test]
1753    fn a_uniformly_referenced_plane_set_is_not_rejected_as_mixed() {
1754        // The control for the test above: two referenced planes must get past
1755        // the mode check (and then fail later, on unimplemented reference
1756        // import). Without this, a guard that rejected everything would look
1757        // correct.
1758        let referenced = BlobPlane {
1759            handle: 7,
1760            offset: 0,
1761            stride: 4,
1762            size: 4,
1763            used: 4,
1764            modifier: 0,
1765            handle_bytes: &[],
1766            data: &[],
1767        };
1768        let blob = build_blob(&[referenced, referenced]);
1769        let err = import(&blob, &[]).expect_err("reference-mode import is not implemented yet");
1770        assert!(
1771            !err.to_string().contains("one transport mode"),
1772            "a uniform plane set must clear the mode check: {err}"
1773        );
1774    }
1775
1776    #[test]
1777    fn an_inline_plane_whose_size_disagrees_with_its_data_is_refused() {
1778        let blob = export(&nv12_mem(64, 48), TransportMode::Inline).unwrap().0;
1779        let mut tampered = blob.clone();
1780        let r = region_offsets(&BlobView::parse(&blob).unwrap().header()).unwrap();
1781        tampered[r.planes + 24..r.planes + 32].copy_from_slice(&9999u64.to_le_bytes());
1782        assert!(import(&tampered, &[]).is_err());
1783    }
1784
1785    #[test]
1786    fn quantization_survives_the_round_trip() {
1787        let mut src =
1788            TensorDyn::from(Tensor::<i8>::new(&[3, 8], Some(TensorMemory::Mem), None).unwrap());
1789        src.set_quantization(crate::Quantization::per_tensor(0.5, -7))
1790            .unwrap();
1791        let blob = export(&src, TransportMode::Inline).unwrap().0;
1792        let got = import(&blob, &[]).unwrap();
1793        let q = got.quantization().expect("quantization survived");
1794        assert_eq!(q.scale(), &[0.5]);
1795        assert_eq!(q.zero_point(), Some(&[-7i32][..]));
1796    }
1797
1798    use crate::{CpuAccess, PixelFormat, Tensor, TensorDyn, TensorMemory};
1799
1800    fn nv12_mem(w: usize, h: usize) -> TensorDyn {
1801        TensorDyn::from(
1802            Tensor::<u8>::image(
1803                w,
1804                h,
1805                PixelFormat::Nv12,
1806                Some(TensorMemory::Mem),
1807                CpuAccess::ReadWrite,
1808            )
1809            .expect("alloc"),
1810        )
1811    }
1812
1813    #[test]
1814    fn export_carries_the_addressing_grid_not_the_allocation() {
1815        let blob = export(&nv12_mem(640, 480), TransportMode::Inline)
1816            .unwrap()
1817            .0;
1818        let v = BlobView::parse(&blob).unwrap();
1819        // The grid, matching the schemas golden's shape for a 640x480 NV12.
1820        assert_eq!(v.shape(), &[480, 640]);
1821        assert_eq!(v.header().dtype, crate::DType::U8.code());
1822        assert_eq!(v.strings().format, "NV12");
1823    }
1824
1825    #[test]
1826    fn export_emits_one_plane_per_plane_table_entry() {
1827        let blob = export(&nv12_mem(640, 480), TransportMode::Inline)
1828            .unwrap()
1829            .0;
1830        let v = BlobView::parse(&blob).unwrap();
1831        let planes = v.planes().unwrap();
1832        assert_eq!(planes.len(), 2, "NV12 is two planes");
1833        // Inline: `size` describes `data`, per the schemas validator's rule.
1834        assert_eq!(planes[0].size as usize, planes[0].data.len());
1835        assert_eq!(planes[1].size as usize, planes[1].data.len());
1836        assert_eq!(planes[0].size, 307_200);
1837        assert_eq!(planes[1].size, 153_600);
1838    }
1839
1840    #[test]
1841    fn inline_export_clears_pid_and_fence_fd() {
1842        // Both are meaningless once the bytes travel: there is no handle to
1843        // reopen and nothing to wait on, because the producer already read it.
1844        let blob = export(&nv12_mem(64, 48), TransportMode::Inline).unwrap().0;
1845        let h = BlobView::parse(&blob).unwrap().header();
1846        assert_eq!(h.pid, 0, "no handle to reopen");
1847        assert_eq!(h.fence_fd, -1, "inlining means the producer already waited");
1848    }
1849
1850    #[test]
1851    fn inline_planes_carry_no_modifier_and_no_handle_bytes() {
1852        // The schemas validator's rule for an inline plane.
1853        let blob = export(&nv12_mem(64, 48), TransportMode::Inline).unwrap().0;
1854        for p in BlobView::parse(&blob).unwrap().planes().unwrap() {
1855            assert!(p.is_inline());
1856            assert_eq!(p.modifier, 0);
1857            assert!(p.handle_bytes.is_empty());
1858            assert_eq!(p.size as usize, p.data.len());
1859        }
1860    }
1861
1862    #[test]
1863    fn reference_export_of_a_mem_tensor_is_refused() {
1864        // `mem` has no shareable handle at all, so reference mode is not
1865        // expressible for it. Refusing beats silently inlining: the caller
1866        // asked for a reference and would otherwise get a copy without knowing.
1867        assert!(export(&nv12_mem(64, 48), TransportMode::Reference).is_err());
1868    }
1869
1870    #[test]
1871    fn a_blob_parses_correctly_from_a_non_zero_offset() {
1872        // CameraFrame embeds a Tensor 8-aligned precisely so a consumer can
1873        // subslice and parse in place, without owning the containing buffer.
1874        let blob = export(&nv12_mem(64, 48), TransportMode::Inline).unwrap().0;
1875        let mut framed = vec![0xAAu8; 16];
1876        framed.extend_from_slice(&blob);
1877        let direct = BlobView::parse(&blob).unwrap();
1878        let framed_view = BlobView::parse(&framed[16..]).unwrap();
1879        assert_eq!(framed_view.header(), direct.header());
1880        assert_eq!(framed_view.shape(), direct.shape());
1881        assert_eq!(framed_view.strings(), direct.strings());
1882    }
1883
1884    #[test]
1885    fn colorimetry_axes_are_carried_as_strings() {
1886        let t = nv12_mem(64, 48).with_colorimetry(crate::Colorimetry::jfif());
1887        let blob = export(&t, TransportMode::Inline).unwrap().0;
1888        let v = BlobView::parse(&blob).unwrap();
1889        let s = v.strings();
1890        assert_eq!(s.color_space, "srgb");
1891        assert_eq!(s.color_transfer, "srgb");
1892        assert_eq!(s.color_encoding, "bt601");
1893        assert_eq!(s.color_range, "full");
1894    }
1895
1896    #[test]
1897    fn a_bare_tensor_carries_no_format_and_its_shape_is_its_grid() {
1898        let t =
1899            TensorDyn::from(Tensor::<f32>::new(&[3, 8], Some(TensorMemory::Mem), None).unwrap());
1900        let blob = export(&t, TransportMode::Inline).unwrap().0;
1901        let v = BlobView::parse(&blob).unwrap();
1902        assert_eq!(v.strings().format, "", "not an image");
1903        assert_eq!(v.shape(), &[3, 8]);
1904        assert_eq!(v.strides(), &[32i64, 4], "byte strides for f32");
1905    }
1906
1907    fn ref_plane() -> BlobPlane<'static> {
1908        BlobPlane {
1909            handle: 7,
1910            offset: 0,
1911            stride: 640,
1912            size: 307_200,
1913            used: 307_200,
1914            modifier: 0,
1915            handle_bytes: &[],
1916            data: &[],
1917        }
1918    }
1919
1920    #[test]
1921    fn a_plane_record_round_trips() {
1922        let mut out = Vec::new();
1923        write_plane(&ref_plane(), &mut out);
1924        let got = parse_planes(&out, 1).unwrap();
1925        assert_eq!(got.len(), 1);
1926        assert_eq!(got[0], ref_plane());
1927    }
1928
1929    #[test]
1930    fn a_plane_scalar_block_is_56_bytes() {
1931        // Matches the schemas spec's independently-stated TensorPlane minimum:
1932        // a 48-byte scalar block plus two 4-byte counts.
1933        let mut out = Vec::new();
1934        write_plane(&ref_plane(), &mut out);
1935        assert_eq!(out.len(), PLANE_RECORD_LEN);
1936        assert_eq!(PLANE_RECORD_LEN, 56);
1937    }
1938
1939    #[test]
1940    fn every_plane_record_starts_8_aligned_whatever_the_payload() {
1941        // Records are read as u64s, so a payload of any length must be padded
1942        // before the next record begins.
1943        for n in 0..17usize {
1944            let payload: Vec<u8> = (0..n as u8).collect();
1945            let mut out = Vec::new();
1946            write_plane(
1947                &BlobPlane {
1948                    handle: -1,
1949                    size: n as u64,
1950                    data: &payload,
1951                    ..ref_plane()
1952                },
1953                &mut out,
1954            );
1955            write_plane(&ref_plane(), &mut out);
1956            assert_eq!(
1957                out.len() % 8,
1958                0,
1959                "payload of {n} bytes left the region unaligned"
1960            );
1961            let got = parse_planes(&out, 2).unwrap();
1962            assert_eq!(got[0].data, &payload[..], "payload {n} survived padding");
1963            assert_eq!(
1964                got[1],
1965                ref_plane(),
1966                "the second record parsed from an aligned offset"
1967            );
1968        }
1969    }
1970
1971    #[test]
1972    fn plane_payloads_round_trip_independently() {
1973        // handle_bytes and data are separate regions; a length applied to the
1974        // wrong one would be invisible if either were empty.
1975        let hb = [0xDEu8, 0xAD, 0xBE, 0xEF];
1976        let data = [1u8, 2, 3];
1977        let mut out = Vec::new();
1978        write_plane(
1979            &BlobPlane {
1980                handle_bytes: &hb,
1981                data: &data,
1982                ..ref_plane()
1983            },
1984            &mut out,
1985        );
1986        let got = parse_planes(&out, 1).unwrap();
1987        assert_eq!(got[0].handle_bytes, &hb[..]);
1988        assert_eq!(got[0].data, &data[..]);
1989    }
1990
1991    #[test]
1992    fn a_forged_plane_payload_length_is_refused() {
1993        let mut out = vec![0u8; PLANE_RECORD_LEN];
1994        out[48..52].copy_from_slice(&u32::MAX.to_le_bytes()); // handle_bytes_len
1995        assert!(matches!(
1996            parse_planes(&out, 1),
1997            Err(BlobError::ForgedLength { .. })
1998        ));
1999    }
2000
2001    #[test]
2002    fn a_plane_count_larger_than_the_records_present_is_refused() {
2003        let mut out = Vec::new();
2004        write_plane(&ref_plane(), &mut out);
2005        assert!(
2006            parse_planes(&out, 2).is_err(),
2007            "claims two records, holds one"
2008        );
2009    }
2010
2011    fn sample_strings() -> BlobStrings<'static> {
2012        BlobStrings {
2013            format: "NV12",
2014            color_space: "bt709",
2015            color_transfer: "bt709",
2016            color_encoding: "bt709",
2017            color_range: "limited",
2018        }
2019    }
2020
2021    #[test]
2022    fn the_five_strings_round_trip_in_message_order() {
2023        let mut out = Vec::new();
2024        write_strings(&sample_strings(), &mut out);
2025        let got = parse_strings(&out).unwrap();
2026        assert_eq!(got, sample_strings());
2027    }
2028
2029    #[test]
2030    fn string_order_is_positional_and_matches_the_message() {
2031        // Order is Tensor.msg's: format, then the four colour axes. Five
2032        // distinct values prove nothing is transposed -- with equal values a
2033        // swap would be invisible.
2034        let s = BlobStrings {
2035            format: "a",
2036            color_space: "bb",
2037            color_transfer: "ccc",
2038            color_encoding: "dddd",
2039            color_range: "eeeee",
2040        };
2041        let mut out = Vec::new();
2042        write_strings(&s, &mut out);
2043        assert_eq!(parse_strings(&out).unwrap(), s);
2044    }
2045
2046    #[test]
2047    fn empty_strings_are_legal_and_distinct_from_absent() {
2048        // "" means "not an image" / "unspecified" -- a real value, not a null.
2049        let s = BlobStrings::default();
2050        let mut out = Vec::new();
2051        write_strings(&s, &mut out);
2052        assert_eq!(parse_strings(&out).unwrap(), s);
2053        assert!(
2054            !out.is_empty(),
2055            "five empty strings still occupy their counts"
2056        );
2057    }
2058
2059    #[test]
2060    fn the_strings_region_is_padded_so_planes_start_8_aligned() {
2061        // Plane records are read as u64s; an unaligned record would fault or
2062        // force byte-at-a-time reads on every consumer.
2063        for f in ["", "a", "ab", "abc", "NV12", "rgb8_planar"] {
2064            let mut out = Vec::new();
2065            write_strings(
2066                &BlobStrings {
2067                    format: f,
2068                    ..Default::default()
2069                },
2070                &mut out,
2071            );
2072            assert_eq!(out.len() % 8, 0, "region for format {f:?} is not 8-aligned");
2073        }
2074    }
2075
2076    #[test]
2077    fn a_non_utf8_string_is_refused() {
2078        // Strings come from a hostile buffer. A lossy substitution would hide
2079        // the malformed input rather than reporting it.
2080        let mut out = Vec::new();
2081        out.extend_from_slice(&2u32.to_le_bytes());
2082        out.extend_from_slice(&[0xff, 0xfe]);
2083        for _ in 0..4 {
2084            out.extend_from_slice(&0u32.to_le_bytes());
2085        }
2086        while out.len() % 8 != 0 {
2087            out.push(0);
2088        }
2089        assert!(matches!(
2090            parse_strings(&out),
2091            Err(BlobError::InvalidUtf8(_))
2092        ));
2093    }
2094
2095    #[test]
2096    fn a_forged_string_length_cannot_read_past_the_region() {
2097        let mut out = Vec::new();
2098        out.extend_from_slice(&u32::MAX.to_le_bytes());
2099        out.extend_from_slice(b"NV12");
2100        while out.len() % 8 != 0 {
2101            out.push(0);
2102        }
2103        assert!(matches!(
2104            parse_strings(&out),
2105            Err(BlobError::ForgedLength { .. })
2106        ));
2107    }
2108
2109    #[test]
2110    fn a_truncated_strings_region_is_refused() {
2111        assert!(parse_strings(&[0u8; 4]).is_err(), "needs five counts");
2112    }
2113
2114    #[test]
2115    fn an_overrunning_region_is_attributed_to_its_own_field() {
2116        // Offsets increase monotonically, so the final `end <= size` check
2117        // already makes over-long regions *safe*. The per-step check exists for
2118        // a different reason: it names the field that was actually implausible.
2119        // Without it every overrun is reported as `planes_bytes`, and a caller
2120        // debugging a forged blob is sent to the wrong field.
2121        let h = BlobHeader {
2122            size: 4096,
2123            ndim: 2,
2124            strides_len: 2,
2125            strings_bytes: u32::MAX,
2126            ..BlobHeader::empty()
2127        };
2128        assert!(
2129            matches!(
2130                region_offsets(&h),
2131                Err(BlobError::ForgedLength {
2132                    field: "strings_bytes",
2133                    ..
2134                })
2135            ),
2136            "the overrun must name strings_bytes, not a later field"
2137        );
2138    }
2139
2140    #[test]
2141    fn header_is_72_bytes_and_every_scalar_is_naturally_aligned() {
2142        assert_eq!(HEADER_LEN, 72);
2143        assert_eq!(HEADER_LEN % 8, 0, "the variable tail starts 8-aligned");
2144        for off in [0usize, 8, 16] {
2145            assert_eq!(off % 8, 0, "u64 field at {off} is not 8-aligned");
2146        }
2147        for off in [24usize, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64] {
2148            assert_eq!(off % 4, 0, "u32 field at {off} is not 4-aligned");
2149        }
2150    }
2151
2152    #[test]
2153    fn a_header_round_trips_through_bytes() {
2154        let h = BlobHeader {
2155            size: 72,
2156            required_mask: 0,
2157            planes_bytes: 0,
2158            storage_kind: 2,
2159            pid: 4242,
2160            fence_fd: -1,
2161            dtype: 1,
2162            quant_axis: -2,
2163            ndim: 2,
2164            plane_count: 0,
2165            quant_scales_len: 0,
2166            quant_zero_points_len: 0,
2167            strings_bytes: 0,
2168            strides_len: 2,
2169        };
2170        let mut buf = vec![0u8; HEADER_LEN];
2171        h.write_to(&mut buf);
2172        assert_eq!(parse_header(&buf).unwrap(), h);
2173    }
2174
2175    #[test]
2176    fn the_header_is_little_endian_on_every_host() {
2177        let mut buf = vec![0u8; HEADER_LEN];
2178        BlobHeader {
2179            size: 0x0102_0304_0506_0708,
2180            ..BlobHeader::empty()
2181        }
2182        .write_to(&mut buf);
2183        assert_eq!(
2184            &buf[0..8],
2185            &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01],
2186            "size must be little-endian regardless of host byte order"
2187        );
2188    }
2189
2190    #[test]
2191    fn a_truncated_buffer_is_refused_not_read() {
2192        assert!(matches!(
2193            parse_header(&[0u8; 71]),
2194            Err(BlobError::Truncated { .. })
2195        ));
2196    }
2197
2198    #[test]
2199    fn an_unknown_required_bit_is_refused() {
2200        let mut buf = vec![0u8; HEADER_LEN];
2201        BlobHeader {
2202            size: 72,
2203            required_mask: 1 << 63,
2204            ..BlobHeader::empty()
2205        }
2206        .write_to(&mut buf);
2207        assert!(matches!(
2208            parse_header(&buf),
2209            Err(BlobError::UnsupportedRequirement(_))
2210        ));
2211    }
2212
2213    #[test]
2214    fn a_size_smaller_than_the_header_is_refused() {
2215        let mut buf = vec![0u8; HEADER_LEN];
2216        BlobHeader {
2217            size: 8,
2218            ..BlobHeader::empty()
2219        }
2220        .write_to(&mut buf);
2221        assert!(
2222            parse_header(&buf).is_err(),
2223            "size must cover at least the header"
2224        );
2225    }
2226}