1use crate::error::RvfError;
8
9pub const DASHBOARD_MAGIC: u32 = 0x5256_4442;
11
12pub const DASHBOARD_MAX_SIZE: u64 = 64 * 1024 * 1024;
14
15#[derive(Clone, Copy, Debug)]
26#[repr(C)]
27pub struct DashboardHeader {
28 pub dashboard_magic: u32,
30 pub header_version: u16,
32 pub ui_framework: u8,
34 pub compression: u8,
36 pub bundle_size: u64,
38 pub file_count: u32,
40 pub entry_path_len: u16,
42 pub reserved: u16,
44 pub build_timestamp: u64,
46 pub content_hash: [u8; 32],
48}
49
50const _: () = assert!(core::mem::size_of::<DashboardHeader>() == 64);
52
53impl DashboardHeader {
54 pub fn to_bytes(&self) -> [u8; 64] {
56 let mut buf = [0u8; 64];
57 buf[0x00..0x04].copy_from_slice(&self.dashboard_magic.to_le_bytes());
58 buf[0x04..0x06].copy_from_slice(&self.header_version.to_le_bytes());
59 buf[0x06] = self.ui_framework;
60 buf[0x07] = self.compression;
61 buf[0x08..0x10].copy_from_slice(&self.bundle_size.to_le_bytes());
62 buf[0x10..0x14].copy_from_slice(&self.file_count.to_le_bytes());
63 buf[0x14..0x16].copy_from_slice(&self.entry_path_len.to_le_bytes());
64 buf[0x16..0x18].copy_from_slice(&self.reserved.to_le_bytes());
65 buf[0x18..0x20].copy_from_slice(&self.build_timestamp.to_le_bytes());
66 buf[0x20..0x40].copy_from_slice(&self.content_hash);
67 buf
68 }
69
70 pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
72 let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
73 if magic != DASHBOARD_MAGIC {
74 return Err(RvfError::BadMagic {
75 expected: DASHBOARD_MAGIC,
76 got: magic,
77 });
78 }
79
80 Ok(Self {
81 dashboard_magic: magic,
82 header_version: u16::from_le_bytes([data[0x04], data[0x05]]),
83 ui_framework: data[0x06],
84 compression: data[0x07],
85 bundle_size: u64::from_le_bytes([
86 data[0x08], data[0x09], data[0x0A], data[0x0B], data[0x0C], data[0x0D], data[0x0E],
87 data[0x0F],
88 ]),
89 file_count: u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]),
90 entry_path_len: u16::from_le_bytes([data[0x14], data[0x15]]),
91 reserved: u16::from_le_bytes([data[0x16], data[0x17]]),
92 build_timestamp: u64::from_le_bytes([
93 data[0x18], data[0x19], data[0x1A], data[0x1B], data[0x1C], data[0x1D], data[0x1E],
94 data[0x1F],
95 ]),
96 content_hash: {
97 let mut h = [0u8; 32];
98 h.copy_from_slice(&data[0x20..0x40]);
99 h
100 },
101 })
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn sample_header() -> DashboardHeader {
110 DashboardHeader {
111 dashboard_magic: DASHBOARD_MAGIC,
112 header_version: 1,
113 ui_framework: 0, compression: 0, bundle_size: 524288,
116 file_count: 12,
117 entry_path_len: 10,
118 reserved: 0,
119 build_timestamp: 1_700_000_000,
120 content_hash: [0xAB; 32],
121 }
122 }
123
124 #[test]
125 fn header_size_is_64() {
126 assert_eq!(core::mem::size_of::<DashboardHeader>(), 64);
127 }
128
129 #[test]
130 fn magic_bytes_match_ascii() {
131 let bytes_be = DASHBOARD_MAGIC.to_be_bytes();
132 assert_eq!(&bytes_be, b"RVDB");
133 }
134
135 #[test]
136 fn round_trip_serialization() {
137 let original = sample_header();
138 let bytes = original.to_bytes();
139 let decoded = DashboardHeader::from_bytes(&bytes).expect("from_bytes should succeed");
140
141 assert_eq!(decoded.dashboard_magic, DASHBOARD_MAGIC);
142 assert_eq!(decoded.header_version, 1);
143 assert_eq!(decoded.ui_framework, 0);
144 assert_eq!(decoded.compression, 0);
145 assert_eq!(decoded.bundle_size, 524288);
146 assert_eq!(decoded.file_count, 12);
147 assert_eq!(decoded.entry_path_len, 10);
148 assert_eq!(decoded.reserved, 0);
149 assert_eq!(decoded.build_timestamp, 1_700_000_000);
150 assert_eq!(decoded.content_hash, [0xAB; 32]);
151 }
152
153 #[test]
154 fn bad_magic_returns_error() {
155 let mut bytes = sample_header().to_bytes();
156 bytes[0] = 0x00; let err = DashboardHeader::from_bytes(&bytes).unwrap_err();
158 match err {
159 RvfError::BadMagic { expected, .. } => assert_eq!(expected, DASHBOARD_MAGIC),
160 other => panic!("expected BadMagic, got {other:?}"),
161 }
162 }
163
164 #[test]
165 fn field_offsets() {
166 let h = sample_header();
167 let base = &h as *const _ as usize;
168
169 assert_eq!(&h.dashboard_magic as *const _ as usize - base, 0x00);
170 assert_eq!(&h.header_version as *const _ as usize - base, 0x04);
171 assert_eq!(&h.ui_framework as *const _ as usize - base, 0x06);
172 assert_eq!(&h.compression as *const _ as usize - base, 0x07);
173 assert_eq!(&h.bundle_size as *const _ as usize - base, 0x08);
174 assert_eq!(&h.file_count as *const _ as usize - base, 0x10);
175 assert_eq!(&h.entry_path_len as *const _ as usize - base, 0x14);
176 assert_eq!(&h.reserved as *const _ as usize - base, 0x16);
177 assert_eq!(&h.build_timestamp as *const _ as usize - base, 0x18);
178 assert_eq!(&h.content_hash as *const _ as usize - base, 0x20);
179 }
180
181 #[test]
182 fn large_bundle_size_round_trip() {
183 let mut h = sample_header();
184 h.bundle_size = DASHBOARD_MAX_SIZE;
185 h.file_count = 500;
186 let bytes = h.to_bytes();
187 let decoded = DashboardHeader::from_bytes(&bytes).unwrap();
188 assert_eq!(decoded.bundle_size, DASHBOARD_MAX_SIZE);
189 assert_eq!(decoded.file_count, 500);
190 }
191}