1use thiserror::Error;
24use tokio::io::AsyncWriteExt;
25
26use crate::framebus::FrameFormat;
27
28pub const TAP_CONTRACT_VERSION: u32 = 2;
31
32pub const TAP_MAGIC: u32 = 0x4144_5654;
34pub const TAP_HEADER_LEN: usize = 24;
36pub const TAP_MAX_FRAME: usize = 64 * 1024 * 1024;
39
40#[derive(Debug, Error, PartialEq, Eq)]
42pub enum TapHeaderError {
43 #[error("bad tap frame magic {0:#x}")]
45 BadMagic(u32),
46 #[error("bad tap format tag {0}")]
48 BadFormatTag(u8),
49 #[error("tap frame of {0} bytes exceeds the {TAP_MAX_FRAME}-byte cap")]
51 FrameTooLarge(usize),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct TapHeader {
57 pub format: FrameFormat,
58 pub width: u32,
59 pub height: u32,
60 pub byte_len: usize,
61 pub ts_mono_ns: u64,
63}
64
65pub fn format_tag(f: FrameFormat) -> u8 {
67 match f {
68 FrameFormat::Rgb24 => 0,
69 FrameFormat::Nv12 => 1,
70 FrameFormat::Yuv420p => 2,
71 FrameFormat::Gray8 => 3,
72 FrameFormat::Gray16 => 4,
73 FrameFormat::BayerRggb8 => 5,
74 FrameFormat::BayerRggb16 => 6,
75 }
76}
77
78pub fn format_from_tag(t: u8) -> Option<FrameFormat> {
80 match t {
81 0 => Some(FrameFormat::Rgb24),
82 1 => Some(FrameFormat::Nv12),
83 2 => Some(FrameFormat::Yuv420p),
84 3 => Some(FrameFormat::Gray8),
85 4 => Some(FrameFormat::Gray16),
86 5 => Some(FrameFormat::BayerRggb8),
87 6 => Some(FrameFormat::BayerRggb16),
88 _ => None,
89 }
90}
91
92pub fn encode_tap_header(
94 format: FrameFormat,
95 width: u32,
96 height: u32,
97 byte_len: u32,
98 ts_mono_ns: u64,
99) -> [u8; TAP_HEADER_LEN] {
100 let mut h = [0u8; TAP_HEADER_LEN];
101 h[0..4].copy_from_slice(&TAP_MAGIC.to_le_bytes());
102 h[4] = format_tag(format);
103 h[6..8].copy_from_slice(&(width as u16).to_le_bytes());
104 h[8..10].copy_from_slice(&(height as u16).to_le_bytes());
105 h[12..16].copy_from_slice(&byte_len.to_le_bytes());
106 h[16..24].copy_from_slice(&ts_mono_ns.to_le_bytes());
107 h
108}
109
110pub fn decode_tap_header(h: &[u8; TAP_HEADER_LEN]) -> Result<TapHeader, TapHeaderError> {
112 let magic = u32::from_le_bytes(h[0..4].try_into().unwrap());
113 if magic != TAP_MAGIC {
114 return Err(TapHeaderError::BadMagic(magic));
115 }
116 let format = format_from_tag(h[4]).ok_or(TapHeaderError::BadFormatTag(h[4]))?;
117 let width = u16::from_le_bytes(h[6..8].try_into().unwrap()) as u32;
118 let height = u16::from_le_bytes(h[8..10].try_into().unwrap()) as u32;
119 let byte_len = u32::from_le_bytes(h[12..16].try_into().unwrap()) as usize;
120 if byte_len > TAP_MAX_FRAME {
121 return Err(TapHeaderError::FrameTooLarge(byte_len));
122 }
123 let ts_mono_ns = u64::from_le_bytes(h[16..24].try_into().unwrap());
124 Ok(TapHeader {
125 format,
126 width,
127 height,
128 byte_len,
129 ts_mono_ns,
130 })
131}
132
133pub async fn write_tap_frame<W>(
136 w: &mut W,
137 format: FrameFormat,
138 width: u32,
139 height: u32,
140 pixels: &[u8],
141 ts_mono_ns: u64,
142) -> std::io::Result<()>
143where
144 W: AsyncWriteExt + Unpin,
145{
146 let header = encode_tap_header(format, width, height, pixels.len() as u32, ts_mono_ns);
147 w.write_all(&header).await?;
148 if !pixels.is_empty() {
149 w.write_all(pixels).await?;
150 }
151 w.flush().await
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
161 fn version_matches_registry() {
162 assert_eq!(
163 TAP_CONTRACT_VERSION as u16,
164 crate::contracts::contract_version("tap.frame").unwrap()
165 );
166 }
167
168 #[test]
169 fn header_round_trips() {
170 for (fmt, w, h, ts) in [
171 (FrameFormat::Rgb24, 640u32, 480u32, 0u64),
172 (FrameFormat::Nv12, 1280, 720, 1),
173 (FrameFormat::Yuv420p, 1920, 1080, u64::MAX),
174 (FrameFormat::Gray8, 640, 480, 123_456_789_012),
175 (FrameFormat::Gray16, 640, 480, 7),
176 (FrameFormat::BayerRggb8, 640, 480, 8),
177 (FrameFormat::BayerRggb16, 640, 480, 9),
178 ] {
179 let len = fmt.frame_bytes(w, h);
180 let hdr = encode_tap_header(fmt, w, h, len as u32, ts);
181 let d = decode_tap_header(&hdr).unwrap();
182 assert_eq!(
183 d,
184 TapHeader {
185 format: fmt,
186 width: w,
187 height: h,
188 byte_len: len,
189 ts_mono_ns: ts,
190 }
191 );
192 }
193 }
194
195 #[test]
196 fn header_layout_is_pinned() {
197 let hdr = encode_tap_header(FrameFormat::Rgb24, 8, 4, 96, 0x0102_0304_0506_0708);
199 assert_eq!(&hdr[0..4], b"TVDA"); assert_eq!(hdr[4], 0); assert_eq!(u16::from_le_bytes([hdr[6], hdr[7]]), 8);
202 assert_eq!(u16::from_le_bytes([hdr[8], hdr[9]]), 4);
203 assert_eq!(u32::from_le_bytes([hdr[12], hdr[13], hdr[14], hdr[15]]), 96);
204 assert_eq!(
205 u64::from_le_bytes(hdr[16..24].try_into().unwrap()),
206 0x0102_0304_0506_0708
207 );
208 }
209
210 #[test]
211 fn decode_rejects_bad_magic_tag_and_oversize() {
212 let mut hdr = encode_tap_header(FrameFormat::Rgb24, 8, 4, 96, 0);
213 let good = hdr;
214 hdr[0] ^= 0xFF;
215 assert!(matches!(
216 decode_tap_header(&hdr),
217 Err(TapHeaderError::BadMagic(_))
218 ));
219 let mut bad_tag = good;
220 bad_tag[4] = 9;
221 assert_eq!(
222 decode_tap_header(&bad_tag),
223 Err(TapHeaderError::BadFormatTag(9))
224 );
225 let mut big = good;
226 big[12..16].copy_from_slice(&((TAP_MAX_FRAME as u32) + 1).to_le_bytes());
227 assert!(matches!(
228 decode_tap_header(&big),
229 Err(TapHeaderError::FrameTooLarge(_))
230 ));
231 }
232}