Skip to main content

viva_genicam/
chunks.rs

1//! Decode GVSP chunk payloads into typed values.
2
3use std::collections::HashMap;
4
5use bytes::{Buf, Bytes};
6use thiserror::Error;
7use tracing::{debug, warn};
8use viva_gige::gvsp::{self, ChunkRaw};
9
10/// Known chunk identifiers defined by the GenICam SFNC.
11const KNOWN_CHUNKS: &[KnownChunk] = &[
12    KnownChunk {
13        id: 0x0001,
14        kind: ChunkKind::Timestamp,
15        decoder: ValueDecoder::U64,
16    },
17    KnownChunk {
18        id: 0x1002,
19        kind: ChunkKind::ExposureTime,
20        decoder: ValueDecoder::F64,
21    },
22    KnownChunk {
23        id: 0x1003,
24        kind: ChunkKind::Gain,
25        decoder: ValueDecoder::F64,
26    },
27    KnownChunk {
28        id: 0x0201,
29        kind: ChunkKind::LineStatusAll,
30        decoder: ValueDecoder::U32,
31    },
32];
33
34#[derive(Copy, Clone)]
35struct KnownChunk {
36    id: u16,
37    kind: ChunkKind,
38    decoder: ValueDecoder,
39}
40
41#[derive(Copy, Clone)]
42enum ValueDecoder {
43    U64,
44    F64,
45    U32,
46}
47
48/// Typed representation of known chunk kinds.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum ChunkKind {
52    Timestamp,
53    ExposureTime,
54    Gain,
55    LineStatusAll,
56    Unknown(u16),
57}
58
59/// Decoded value of a chunk entry.
60#[derive(Debug, Clone, PartialEq)]
61#[non_exhaustive]
62pub enum ChunkValue {
63    U64(u64),
64    F64(f64),
65    U32(u32),
66    Bytes(Bytes),
67}
68
69pub type ChunkMap = HashMap<ChunkKind, ChunkValue>;
70
71/// Errors that can occur while decoding chunk payloads.
72#[derive(Debug, Error)]
73#[non_exhaustive]
74pub enum ChunkError {
75    #[error("chunk {id:#06x} payload length {actual} shorter than required {expected} bytes")]
76    InvalidLength {
77        id: u16,
78        expected: usize,
79        actual: usize,
80    },
81}
82
83fn decode_known(raw: &ChunkRaw, entry: &KnownChunk) -> Result<(ChunkKind, ChunkValue), ChunkError> {
84    let expected = match entry.decoder {
85        ValueDecoder::U64 | ValueDecoder::F64 => 8,
86        ValueDecoder::U32 => 4,
87    };
88    if raw.data.len() < expected {
89        warn!(
90            chunk_id = format_args!("{:#06x}", raw.id),
91            len = raw.data.len(),
92            expected,
93            "truncated chunk payload"
94        );
95        return Err(ChunkError::InvalidLength {
96            id: raw.id,
97            expected,
98            actual: raw.data.len(),
99        });
100    }
101    let mut cursor = raw.data.clone();
102    let value = match entry.decoder {
103        ValueDecoder::U64 => ChunkValue::U64(cursor.get_u64_le()),
104        ValueDecoder::F64 => ChunkValue::F64(cursor.get_f64_le()),
105        ValueDecoder::U32 => ChunkValue::U32(cursor.get_u32_le()),
106    };
107    debug!(
108        chunk_id = format_args!("{:#06x}", raw.id),
109        len = raw.data.len(),
110        kind = ?entry.kind,
111        "decoded known chunk"
112    );
113    Ok((entry.kind, value))
114}
115
116/// Decode raw chunk entries into typed values.
117pub fn decode_raw_chunks(chunks: &[ChunkRaw]) -> Result<ChunkMap, ChunkError> {
118    let mut map = HashMap::new();
119    for chunk in chunks {
120        if let Some(entry) = KNOWN_CHUNKS
121            .iter()
122            .find(|candidate| candidate.id == chunk.id)
123        {
124            let (kind, value) = decode_known(chunk, entry)?;
125            map.insert(kind, value);
126        } else {
127            debug!(
128                chunk_id = format_args!("{:#06x}", chunk.id),
129                len = chunk.data.len(),
130                "storing unknown chunk"
131            );
132            map.insert(
133                ChunkKind::Unknown(chunk.id),
134                ChunkValue::Bytes(chunk.data.clone()),
135            );
136        }
137    }
138    Ok(map)
139}
140
141/// Parse raw bytes into chunks and decode known values.
142pub fn parse_chunk_bytes(data: &[u8]) -> Result<ChunkMap, ChunkError> {
143    let raw = gvsp::parse_chunks(data);
144    decode_raw_chunks(&raw)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    fn chunk_buffer(id: u16, payload: &[u8]) -> Vec<u8> {
152        let mut buf = Vec::new();
153        buf.extend_from_slice(&id.to_be_bytes());
154        buf.extend_from_slice(&0u16.to_be_bytes());
155        buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
156        buf.extend_from_slice(payload);
157        buf
158    }
159
160    #[test]
161    fn decode_known_chunks() {
162        let mut data = Vec::new();
163        data.extend_from_slice(&chunk_buffer(
164            0x0001,
165            &0x1234_5678_9ABC_DEF0u64.to_le_bytes(),
166        ));
167        data.extend_from_slice(&chunk_buffer(0x1002, &1234.5f64.to_le_bytes()));
168        let map = parse_chunk_bytes(&data).expect("decode");
169        assert!(matches!(
170            map.get(&ChunkKind::Timestamp),
171            Some(ChunkValue::U64(0x1234_5678_9ABC_DEF0))
172        ));
173        assert!(matches!(
174            map.get(&ChunkKind::ExposureTime),
175            Some(ChunkValue::F64(v)) if (*v - 1234.5).abs() < f64::EPSILON
176        ));
177    }
178
179    #[test]
180    fn invalid_length_errors() {
181        let data = chunk_buffer(0x0001, &[0x12, 0x34]);
182        let err = parse_chunk_bytes(&data).unwrap_err();
183        assert!(matches!(err, ChunkError::InvalidLength { id: 0x0001, .. }));
184    }
185
186    #[test]
187    fn unknown_chunk_kept_as_bytes() {
188        let payload = [0xAA, 0xBB, 0xCC];
189        let data = chunk_buffer(0xDEAD, &payload);
190        let map = parse_chunk_bytes(&data).expect("decode");
191        assert!(matches!(
192            map.get(&ChunkKind::Unknown(0xDEAD)),
193            Some(ChunkValue::Bytes(bytes)) if bytes.as_ref() == payload
194        ));
195    }
196
197    const KNOWN_IDS: &[u16] = &[0x0001, 0x1002, 0x1003, 0x0201];
198
199    #[test]
200    fn random_unknown_chunks_are_stored() {
201        for _ in 0..128 {
202            let mut id = fastrand::u16(..);
203            while KNOWN_IDS.contains(&id) {
204                id = fastrand::u16(..);
205            }
206            let len = fastrand::usize(..16);
207            let mut payload = vec![0u8; len];
208            for byte in &mut payload {
209                *byte = fastrand::u8(..);
210            }
211            let mut buffer = chunk_buffer(id, &payload);
212            let padding_len = fastrand::usize(..8);
213            for _ in 0..padding_len {
214                buffer.push(fastrand::u8(..));
215            }
216            let raw = gvsp::parse_chunks(&buffer);
217            let map = decode_raw_chunks(&raw).expect("decode");
218            match map.get(&ChunkKind::Unknown(id)) {
219                Some(ChunkValue::Bytes(bytes)) => assert_eq!(bytes.as_ref(), payload.as_slice()),
220                Some(other) => panic!("expected raw bytes for unknown chunk, found {other:?}"),
221                None => panic!("unknown chunk missing from map"),
222            }
223        }
224    }
225}