1use crate::varint::decode_varint;
2use crate::Error;
3use std::collections::HashMap;
4
5const MAGIC: [u8; 4] = [0x89, 0x48, 0x50, 0x4B];
7const VERSION: u8 = 0x01;
8const FLAG_MINIFIED: u8 = 1 << 0;
9const FLAG_CHECKSUM: u8 = 1 << 1;
10const FLAG_COMPRESSION_MASK: u8 = 0b00001100;
11const FLAG_COMPRESSION_SHIFT: u8 = 2;
12const CRC32_SIZE: usize = 4;
13const MIN_PACKET_SIZE: usize = 7; const FIELD_CUSTOM_START: u8 = 0x10;
15
16const FIELD_URL: u8 = 0x01;
18const FIELD_ETAG: u8 = 0x02;
19const FIELD_SIGNATURE: u8 = 0x03;
20const FIELD_CONTENT_TYPE: u8 = 0x04;
21const FIELD_TIMESTAMP: u8 = 0x05;
22const FIELD_ENCODING: u8 = 0x06;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CompressionAlgorithm {
27 Deflate,
28 Gzip,
29}
30
31#[derive(Debug)]
33pub struct UnpackResult {
34 pub url: String,
35 pub etag: Option<String>,
36 pub signature: Option<String>,
37 pub content_type: Option<String>,
38 pub timestamp: Option<u64>,
39 pub encoding: Option<String>,
40 pub custom: HashMap<String, String>,
41 pub html: String,
42 pub version: u8,
43 pub minified: bool,
44 pub compression: CompressionAlgorithm,
45 pub checksum_valid: Option<bool>,
46}
47
48pub struct UnpackOptions {
50 pub verify_checksum: bool,
51 pub headers_only: bool,
52}
53
54impl Default for UnpackOptions {
55 fn default() -> Self {
56 Self {
57 verify_checksum: true,
58 headers_only: false,
59 }
60 }
61}
62
63struct HeaderField {
65 field_type: u8,
66 name: Option<String>,
67 value: String,
68}
69
70#[allow(clippy::type_complexity)]
72fn decode_packet(
73 data: &[u8],
74) -> Result<
75 (
76 u8, bool, bool, CompressionAlgorithm, Vec<HeaderField>, &[u8], usize, Option<u32>, ),
85 Error,
86> {
87 if data.len() < MIN_PACKET_SIZE {
88 return Err(Error::TruncatedPacket(format!(
89 "Packet too small: {} bytes (minimum: {})",
90 data.len(),
91 MIN_PACKET_SIZE
92 )));
93 }
94
95 let mut offset = 0;
96
97 if data[0..4] != MAGIC {
99 return Err(Error::InvalidMagic);
100 }
101 offset += 4;
102
103 let version = data[offset];
105 if version != VERSION {
106 return Err(Error::UnsupportedVersion(version));
107 }
108 offset += 1;
109
110 let flags_byte = data[offset];
112 offset += 1;
113 let minified = flags_byte & FLAG_MINIFIED != 0;
114 let has_checksum = flags_byte & FLAG_CHECKSUM != 0;
115 let compression_id = (flags_byte & FLAG_COMPRESSION_MASK) >> FLAG_COMPRESSION_SHIFT;
116 let compression = match compression_id {
117 1 => CompressionAlgorithm::Gzip,
118 _ => CompressionAlgorithm::Deflate,
119 };
120
121 let (header_len, n) = decode_varint(data, offset)?;
123 offset += n;
124 let header_len = header_len as usize;
125 let header_end = offset + header_len;
126
127 if header_end > data.len() {
128 return Err(Error::TruncatedPacket(
129 "Header section extends beyond packet".into(),
130 ));
131 }
132
133 let (field_count, n) = decode_varint(data, offset)?;
135 offset += n;
136
137 let mut fields = Vec::with_capacity(field_count as usize);
139 for i in 0..field_count as usize {
140 if offset >= header_end {
141 return Err(Error::TruncatedPacket(format!(
142 "Header field {} extends beyond header section",
143 i
144 )));
145 }
146
147 let field_type = data[offset];
148 offset += 1;
149
150 if field_type >= FIELD_CUSTOM_START {
151 if offset >= header_end {
153 return Err(Error::TruncatedPacket(format!(
154 "Custom field {} name length missing",
155 i
156 )));
157 }
158 let name_len = data[offset] as usize;
159 offset += 1;
160
161 if offset + name_len > header_end {
162 return Err(Error::TruncatedPacket(format!(
163 "Custom field {} name truncated",
164 i
165 )));
166 }
167 let name = String::from_utf8_lossy(&data[offset..offset + name_len]).into_owned();
168 offset += name_len;
169
170 let (value_len, n) = decode_varint(data, offset)?;
171 offset += n;
172 let value_len = value_len as usize;
173
174 if offset + value_len > header_end {
175 return Err(Error::TruncatedPacket(format!(
176 "Custom field {} value truncated",
177 i
178 )));
179 }
180 let value = String::from_utf8_lossy(&data[offset..offset + value_len]).into_owned();
181 offset += value_len;
182
183 fields.push(HeaderField {
184 field_type,
185 name: Some(name),
186 value,
187 });
188 } else {
189 let (value_len, n) = decode_varint(data, offset)?;
191 offset += n;
192 let value_len = value_len as usize;
193
194 if offset + value_len > header_end {
195 return Err(Error::TruncatedPacket(format!(
196 "Field {} (type 0x{:02X}) value truncated",
197 i, field_type
198 )));
199 }
200 let value = String::from_utf8_lossy(&data[offset..offset + value_len]).into_owned();
201 offset += value_len;
202
203 fields.push(HeaderField {
204 field_type,
205 name: None,
206 value,
207 });
208 }
209 }
210
211 offset = header_end;
213
214 if offset >= data.len() {
216 return Err(Error::TruncatedPacket("Body section missing".into()));
217 }
218
219 let (uncompressed_len, n) = decode_varint(data, offset)?;
220 offset += n;
221
222 let crc_size = if has_checksum { CRC32_SIZE } else { 0 };
223 let compressed_body_end = data.len() - crc_size;
224
225 if offset > compressed_body_end {
226 return Err(Error::TruncatedPacket("Compressed body truncated".into()));
227 }
228
229 let compressed_body = &data[offset..compressed_body_end];
230
231 let crc32 = if has_checksum {
232 let crc_offset = compressed_body_end;
233 if crc_offset + CRC32_SIZE > data.len() {
234 return Err(Error::TruncatedPacket("CRC32 checksum truncated".into()));
235 }
236 Some(
237 data[crc_offset] as u32
238 | (data[crc_offset + 1] as u32) << 8
239 | (data[crc_offset + 2] as u32) << 16
240 | (data[crc_offset + 3] as u32) << 24,
241 )
242 } else {
243 None
244 };
245
246 Ok((
247 version,
248 minified,
249 has_checksum,
250 compression,
251 fields,
252 compressed_body,
253 uncompressed_len as usize,
254 crc32,
255 ))
256}
257
258#[allow(clippy::type_complexity)]
260fn extract_fields(
261 fields: &[HeaderField],
262) -> (
263 String,
264 Option<String>,
265 Option<String>,
266 Option<String>,
267 Option<u64>,
268 Option<String>,
269 HashMap<String, String>,
270) {
271 let mut url = String::new();
272 let mut etag = None;
273 let mut signature = None;
274 let mut content_type = None;
275 let mut timestamp = None;
276 let mut encoding = None;
277 let mut custom = HashMap::new();
278
279 for field in fields {
280 match field.field_type {
281 FIELD_URL => url = field.value.clone(),
282 FIELD_ETAG => etag = Some(field.value.clone()),
283 FIELD_SIGNATURE => signature = Some(field.value.clone()),
284 FIELD_CONTENT_TYPE => content_type = Some(field.value.clone()),
285 FIELD_TIMESTAMP => timestamp = field.value.parse().ok(),
286 FIELD_ENCODING => encoding = Some(field.value.clone()),
287 t if t >= FIELD_CUSTOM_START => {
288 if let Some(name) = &field.name {
289 custom.insert(name.clone(), field.value.clone());
290 }
291 }
292 _ => {} }
294 }
295
296 (
297 url,
298 etag,
299 signature,
300 content_type,
301 timestamp,
302 encoding,
303 custom,
304 )
305}
306
307pub fn unpack(data: &[u8]) -> Result<UnpackResult, Error> {
309 unpack_with_options(data, &UnpackOptions::default())
310}
311
312pub fn unpack_with_options(data: &[u8], options: &UnpackOptions) -> Result<UnpackResult, Error> {
314 let (
315 version,
316 minified,
317 _has_checksum,
318 compression,
319 fields,
320 compressed_body,
321 _uncompressed_len,
322 crc32_value,
323 ) = decode_packet(data)?;
324
325 let (url, etag, signature, content_type, timestamp, encoding, custom) = extract_fields(&fields);
326
327 if options.headers_only {
328 return Ok(UnpackResult {
329 url,
330 etag,
331 signature,
332 content_type,
333 timestamp,
334 encoding,
335 custom,
336 html: String::new(),
337 version,
338 minified,
339 compression,
340 checksum_valid: None,
341 });
342 }
343
344 let decompressed = decompress(compressed_body, compression)?;
346
347 let checksum_valid = if let Some(expected) = crc32_value {
349 let actual = crc32fast::hash(&decompressed);
350 if options.verify_checksum && actual != expected {
351 return Err(Error::ChecksumMismatch { expected, actual });
352 }
353 Some(actual == expected)
354 } else {
355 None
356 };
357
358 let html = String::from_utf8(decompressed).map_err(Error::InvalidUtf8)?;
359
360 Ok(UnpackResult {
361 url,
362 etag,
363 signature,
364 content_type,
365 timestamp,
366 encoding,
367 custom,
368 html,
369 version,
370 minified,
371 compression,
372 checksum_valid,
373 })
374}
375
376pub fn read_headers(data: &[u8]) -> Result<UnpackResult, Error> {
378 unpack_with_options(
379 data,
380 &UnpackOptions {
381 verify_checksum: false,
382 headers_only: true,
383 },
384 )
385}
386
387fn decompress(data: &[u8], algorithm: CompressionAlgorithm) -> Result<Vec<u8>, Error> {
389 use std::io::Read;
390
391 match algorithm {
392 CompressionAlgorithm::Gzip => {
393 let mut decoder = flate2::read::GzDecoder::new(data);
394 let mut buf = Vec::new();
395 decoder
396 .read_to_end(&mut buf)
397 .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
398 Ok(buf)
399 }
400 CompressionAlgorithm::Deflate => {
401 let mut decoder = flate2::read::DeflateDecoder::new(data);
402 let mut buf = Vec::new();
403 decoder
404 .read_to_end(&mut buf)
405 .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
406 Ok(buf)
407 }
408 }
409}