wowlab_parsers/parsers/parcel/
codec.rs1use std::{
4 fmt::Write as _,
5 io::{Read, Write},
6};
7
8use flate2::{Compression, read::DeflateDecoder, write::DeflateEncoder};
9use serde::{Deserialize, Serialize};
10
11use super::{
12 base64::{self, is_base64_alphabet},
13 crc32::{crc32_hex8, is_checksum_token},
14 errors::{CodecBuildError, ParcelError},
15};
16
17pub const DEFAULT_MAX_ENCODED_LEN: usize = 100_000;
19pub const DEFAULT_MAX_DECODED_BODY: usize = 65_536;
21pub const DEFAULT_MAX_PAYLOAD_BYTES: usize = 262_144;
23
24const FIELD_SEPARATOR: char = '.';
25const FIELD_SEPARATOR_LEN: usize = 1;
26const CHECKSUM_LEN: usize = 8;
27const READ_CHUNK: usize = 8 * 1024;
28
29#[derive(Clone, Debug)]
31pub struct CodecConfig {
32 pub prefix: String,
33 pub content_types: Vec<String>,
34 pub default_content_type: Option<String>,
35 pub max_encoded_len: Option<usize>,
36 pub max_decoded_body: Option<usize>,
37 pub max_payload_bytes: Option<usize>,
38}
39
40impl CodecConfig {
41 pub fn new(prefix: impl Into<String>) -> Self {
43 Self {
44 prefix: prefix.into(),
45 content_types: Vec::new(),
46 default_content_type: None,
47 max_encoded_len: None,
48 max_decoded_body: None,
49 max_payload_bytes: None,
50 }
51 }
52}
53
54#[derive(Clone, Debug)]
56pub struct Codec {
57 prefix: String,
58 content_types: Vec<Box<str>>,
59 default_ct: String,
60 max_encoded_len: usize,
61 max_decoded_body: usize,
62 max_payload_bytes: usize,
63}
64
65#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub struct DecodeMeta {
68 pub prefix: String,
69 pub content_type: String,
70 pub checksum: String,
71 pub encoded_bytes: usize,
72 pub decoded_body_bytes: usize,
73 pub payload_bytes: usize,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct DecodedParcel {
79 pub payload: Vec<u8>,
80 pub meta: DecodeMeta,
81}
82
83impl Codec {
84 pub fn new(config: CodecConfig) -> Result<Self, CodecBuildError> {
90 if config.prefix.is_empty() {
91 return Err(CodecBuildError::empty_prefix());
92 }
93
94 if config.prefix.contains(FIELD_SEPARATOR) {
95 return Err(CodecBuildError::prefix_contains_dot());
96 }
97
98 let mut content_types = config.content_types;
99
100 if content_types.is_empty() {
101 content_types.push("bin".to_owned());
102 }
103
104 if let Some(bad) = content_types.iter().find(|ct| !is_valid_content_type(ct)) {
105 return Err(CodecBuildError::invalid_content_type(bad.clone()));
106 }
107
108 let default_ct = match config.default_content_type {
109 Some(ct) => {
110 if !content_types.iter().any(|c| c == &ct) {
111 return Err(CodecBuildError::invalid_default_content_type(ct));
112 }
113
114 ct
115 }
116 None => content_types
117 .first()
118 .cloned()
119 .ok_or_else(CodecBuildError::empty_content_types)?,
120 };
121
122 let content_types = content_types
123 .into_iter()
124 .map(String::into_boxed_str)
125 .collect();
126
127 Ok(Self {
128 prefix: config.prefix,
129 content_types,
130 default_ct,
131 max_encoded_len: config.max_encoded_len.unwrap_or(DEFAULT_MAX_ENCODED_LEN),
132 max_decoded_body: config.max_decoded_body.unwrap_or(DEFAULT_MAX_DECODED_BODY),
133 max_payload_bytes: config
134 .max_payload_bytes
135 .unwrap_or(DEFAULT_MAX_PAYLOAD_BYTES),
136 })
137 }
138
139 #[must_use]
141 pub fn prefix(&self) -> &str {
142 &self.prefix
143 }
144
145 #[must_use]
147 pub fn default_content_type(&self) -> &str {
148 &self.default_ct
149 }
150
151 pub fn encode(
157 &self,
158 payload: &[u8],
159 content_type: Option<&str>,
160 ) -> Result<String, ParcelError> {
161 let content_type = content_type.unwrap_or(&self.default_ct);
162
163 if !self
164 .content_types
165 .iter()
166 .any(|candidate| candidate.as_ref() == content_type)
167 {
168 return Err(ParcelError::ContentType);
169 }
170
171 if payload.len() > self.max_payload_bytes {
172 return Err(ParcelError::PayloadTooLarge);
173 }
174
175 let body_bytes = compress_raw_deflate(payload)?;
176
177 if body_bytes.is_empty() {
178 return Err(ParcelError::Compress);
179 }
180
181 if body_bytes.len() > self.max_decoded_body {
182 return Err(ParcelError::BodyTooLarge);
183 }
184
185 let checksum = crc32_hex8(payload);
186 let body_b64 = base64::encode(&body_bytes);
187
188 let mut encoded = String::with_capacity(
189 self.prefix.len()
190 + content_type.len()
191 + FIELD_SEPARATOR_LEN
192 + CHECKSUM_LEN
193 + FIELD_SEPARATOR_LEN
194 + body_b64.len(),
195 );
196
197 encoded.push_str(&self.prefix);
198 encoded.push_str(content_type);
199 encoded.push(FIELD_SEPARATOR);
200 encoded.push_str(&checksum);
201 encoded.push(FIELD_SEPARATOR);
202 encoded.push_str(&body_b64);
203
204 if encoded.len() > self.max_encoded_len {
205 return Err(ParcelError::EncodedTooLarge);
206 }
207
208 Ok(encoded)
209 }
210
211 pub fn decode(&self, encoded: &str) -> Result<DecodedParcel, ParcelError> {
217 let (content_type, checksum_hex, body) = self.parse(encoded)?;
218
219 let body_bytes = base64::decode(body)?;
220
221 if body_bytes.len() > self.max_decoded_body {
222 return Err(ParcelError::BodyTooLarge);
223 }
224
225 let payload = decompress_raw_deflate(&body_bytes, self.max_payload_bytes)?;
226
227 if crc32_hex8(&payload) != checksum_hex {
228 return Err(ParcelError::ChecksumMismatch);
229 }
230
231 Ok(DecodedParcel {
232 meta: DecodeMeta {
233 prefix: self.prefix.clone(),
234 content_type: content_type.to_owned(),
235 checksum: checksum_hex.to_owned(),
236 encoded_bytes: encoded.len(),
237 decoded_body_bytes: body_bytes.len(),
238 payload_bytes: payload.len(),
239 },
240 payload,
241 })
242 }
243
244 #[must_use]
246 pub fn inspect(&self, encoded: &str) -> String {
247 match self.decode(encoded) {
248 Err(e) => format!("LibParcel inspect: {}", e.code()),
249 Ok(decoded) => format_inspect(&decoded),
250 }
251 }
252
253 fn parse<'a>(&self, encoded: &'a str) -> Result<(&'a str, &'a str, &'a str), ParcelError> {
254 if encoded.len() > self.max_encoded_len {
255 return Err(ParcelError::EncodedTooLarge);
256 }
257
258 let rest = encoded
259 .strip_prefix(self.prefix.as_str())
260 .ok_or(ParcelError::Prefix)?;
261
262 let mut parts = rest.split(FIELD_SEPARATOR);
263 let content_type = parts.next().ok_or(ParcelError::Fields)?;
264 let checksum_hex = parts.next().ok_or(ParcelError::Fields)?;
265 let body = parts.next().ok_or(ParcelError::Fields)?;
266
267 if parts.next().is_some() {
268 return Err(ParcelError::Fields);
269 }
270
271 if !self
272 .content_types
273 .iter()
274 .any(|candidate| candidate.as_ref() == content_type)
275 {
276 return Err(ParcelError::ContentType);
277 }
278
279 if !is_checksum_token(checksum_hex) {
280 return Err(ParcelError::ChecksumToken);
281 }
282
283 if body.is_empty() || !is_base64_alphabet(body) {
284 return Err(ParcelError::Base64Token);
285 }
286
287 Ok((content_type, checksum_hex, body))
288 }
289}
290
291fn is_valid_content_type(ct: &str) -> bool {
292 let mut bytes = ct.bytes();
293 let Some(first) = bytes.next() else {
294 return false;
295 };
296
297 if !first.is_ascii_lowercase() {
298 return false;
299 }
300
301 bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
302}
303
304fn compress_raw_deflate(payload: &[u8]) -> Result<Vec<u8>, ParcelError> {
306 let mut encoder =
307 DeflateEncoder::new(Vec::with_capacity(payload.len()), Compression::default());
308
309 encoder
310 .write_all(payload)
311 .map_err(|_write_error| ParcelError::Compress)?;
312
313 encoder
314 .finish()
315 .map_err(|_finish_error| ParcelError::Compress)
316}
317
318fn decompress_raw_deflate(body: &[u8], max_payload_bytes: usize) -> Result<Vec<u8>, ParcelError> {
320 let mut decoder = DeflateDecoder::new(body);
321 let mut output = Vec::new();
322 let mut buf = [0u8; READ_CHUNK];
323
324 loop {
325 match decoder.read(&mut buf) {
326 Ok(0) => return Ok(output),
327 Ok(n) => {
328 let new_len = output.len().saturating_add(n);
329
330 if new_len > max_payload_bytes {
331 return Err(ParcelError::PayloadTooLarge);
332 }
333 output.extend_from_slice(&buf[..n]);
336 }
337 Err(_) => return Err(ParcelError::Decompress),
338 }
339 }
340}
341
342const PREVIEW_BYTES: usize = 32;
343const LABEL_WIDTH: usize = 19;
344
345fn format_inspect(decoded: &DecodedParcel) -> String {
346 const HIGH_NIBBLE_SHIFT: u32 = 4;
347 const LOW_NIBBLE_MASK: u8 = 0x0F;
348
349 let m = &decoded.meta;
350 let mut out = String::from("LibParcel inspect:\n");
351 let _ = writeln!(out, " {:<LABEL_WIDTH$} = {}", "prefix", m.prefix);
352 let _ = writeln!(
353 out,
354 " {:<LABEL_WIDTH$} = {}",
355 "content_type", m.content_type
356 );
357 let _ = writeln!(out, " {:<LABEL_WIDTH$} = {}", "checksum", m.checksum);
358 let _ = writeln!(
359 out,
360 " {:<LABEL_WIDTH$} = {}",
361 "encoded_bytes", m.encoded_bytes
362 );
363 let _ = writeln!(
364 out,
365 " {:<LABEL_WIDTH$} = {}",
366 "decoded_body_bytes", m.decoded_body_bytes
367 );
368 let _ = write!(
369 out,
370 " {:<LABEL_WIDTH$} = {}",
371 "payload_bytes", m.payload_bytes
372 );
373
374 if !decoded.payload.is_empty() {
375 const HEX_BYTES_PER_INPUT: usize = 3;
376 let n = decoded.payload.len().min(PREVIEW_BYTES);
377 let mut hex = String::with_capacity(n * HEX_BYTES_PER_INPUT);
378 let mut ascii = String::with_capacity(n);
379
380 for (i, &b) in decoded.payload.iter().take(n).enumerate() {
381 if i > 0 {
382 hex.push(' ');
383 }
384
385 hex.push(hex_nibble(b >> HIGH_NIBBLE_SHIFT));
386 hex.push(hex_nibble(b & LOW_NIBBLE_MASK));
387 let printable = b.is_ascii_graphic() || b == b' ';
388
389 ascii.push(if printable { b as char } else { '.' });
390 }
391
392 out.push('\n');
393 let _ = writeln!(out, " {:<LABEL_WIDTH$} = {hex}", "payload_hex_preview");
394 let _ = write!(out, " {:<LABEL_WIDTH$} = {ascii}", "payload_ascii");
395 }
396
397 out
398}
399
400fn hex_nibble(n: u8) -> char {
401 const HEX_DIGIT_MAX: u8 = 9;
402
403 if n <= HEX_DIGIT_MAX {
404 (b'0' + n) as char
405 } else {
406 const HEX_LETTER_BASE: u8 = 10;
407
408 (b'a' + (n - HEX_LETTER_BASE)) as char
409 }
410}