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