#![allow(dead_code)]
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
pub fn decode_hdrp_makernote(b64_data: &str) -> Vec<Tag> {
let raw = match base64_decode(b64_data.trim()) {
Some(v) => v,
None => return Vec::new(),
};
let decompressed = match hdrp_decrypt_gunzip(&raw) {
Some(v) => v,
None => return Vec::new(),
};
parse_hdrp_protobuf(&decompressed)
}
fn base64_decode(s: &str) -> Option<Vec<u8>> {
const TABLE: [i8; 256] = {
let mut t = [-1i8; 256];
let mut i = 0u8;
while i < 26 {
t[(b'A' + i) as usize] = i as i8;
i += 1;
}
i = 0;
while i < 26 {
t[(b'a' + i) as usize] = (i + 26) as i8;
i += 1;
}
i = 0;
while i < 10 {
t[(b'0' + i) as usize] = (i + 52) as i8;
i += 1;
}
t[b'+' as usize] = 62;
t[b'/' as usize] = 63;
t[b'=' as usize] = 0; t
};
let bytes: Vec<u8> = s.bytes().filter(|&b| !b.is_ascii_whitespace()).collect();
if bytes.is_empty() {
return None;
}
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
let mut i = 0;
while i + 3 < bytes.len() {
let a = TABLE[bytes[i] as usize];
let b = TABLE[bytes[i + 1] as usize];
let c = TABLE[bytes[i + 2] as usize];
let d = TABLE[bytes[i + 3] as usize];
if a < 0 || b < 0 {
return None;
}
out.push(((a as u8) << 2) | ((b as u8) >> 4));
if bytes[i + 2] != b'=' {
out.push(((b as u8) << 4) | ((c as u8) >> 2));
}
if bytes[i + 3] != b'=' {
out.push(((c as u8) << 6) | (d as u8));
}
i += 4;
}
Some(out)
}
fn hdrp_decrypt(data: &[u8]) -> Option<Vec<u8>> {
if data.len() < 5 || &data[0..4] != b"HDRP" {
return None;
}
let _ver = data[4];
let payload = &data[5..];
let pad = (8usize.wrapping_sub(payload.len() % 8)) & 7;
let mut words: Vec<u32> = {
let mut padded = payload.to_vec();
padded.extend_from_slice(&[0u8; 8][..pad]);
padded
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
};
let mut hi: u64 = 0x2515606b;
let mut lo: u64 = 0x4a7791cd;
let mut i = 0;
while i < words.len() {
let new_lo = lo ^ ((lo >> 12) | ((hi & 0xfff) << 20));
let new_hi = hi ^ (hi >> 12);
lo = new_lo & 0xffffffff;
hi = new_hi & 0xffffffff;
let new_hi = hi ^ (((hi & 0x7f) << 25) | (lo >> 7));
let new_lo = lo ^ ((lo & 0x7f) << 25);
lo = new_lo & 0xffffffff;
hi = new_hi & 0xffffffff;
let new_lo = lo ^ ((lo >> 27) | ((hi & 0x7ffffff) << 5));
let new_hi = hi ^ (hi >> 27);
lo = new_lo & 0xffffffff;
hi = new_hi & 0xffffffff;
let key64: u64 = (hi << 32) | lo;
let product = (key64 as u128).wrapping_mul(0x2545f4914f6cdd1d_u128);
let result64 = product as u64;
lo = result64 & 0xffffffff;
hi = (result64 >> 32) & 0xffffffff;
words[i] ^= lo as u32;
i += 1;
if i < words.len() {
words[i] ^= hi as u32;
i += 1;
}
}
let mut result: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
if pad > 0 {
result.truncate(result.len().saturating_sub(pad));
}
Some(result)
}
struct BitReader<'a> {
data: &'a [u8],
pos: usize, bits: u32, nbits: u32, }
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
BitReader {
data,
pos: 0,
bits: 0,
nbits: 0,
}
}
fn fill(&mut self) {
while self.nbits <= 24 && self.pos < self.data.len() {
self.bits |= (self.data[self.pos] as u32) << self.nbits;
self.pos += 1;
self.nbits += 8;
}
}
fn read_bits(&mut self, n: u32) -> Option<u32> {
if n == 0 {
return Some(0);
}
self.fill();
if self.nbits < n {
return None;
}
let val = self.bits & ((1u32 << n) - 1);
self.bits >>= n;
self.nbits -= n;
Some(val)
}
fn align_byte(&mut self) {
let rem = self.nbits & 7;
if rem != 0 {
self.bits >>= rem;
self.nbits -= rem;
}
}
fn read_aligned_byte(&mut self) -> Option<u8> {
if self.nbits >= 8 {
let b = (self.bits & 0xff) as u8;
self.bits >>= 8;
self.nbits -= 8;
Some(b)
} else {
if self.pos < self.data.len() {
let b = self.data[self.pos];
self.pos += 1;
Some(b)
} else {
None
}
}
}
fn read_aligned_u16_le(&mut self) -> Option<u16> {
let lo = self.read_aligned_byte()? as u16;
let hi = self.read_aligned_byte()? as u16;
Some(lo | (hi << 8))
}
}
fn build_huffman(lengths: &[u8]) -> Option<(Vec<(u16, u8)>, u8)> {
let max_bits = *lengths.iter().max()? as usize;
if max_bits == 0 {
return Some((vec![], 0));
}
let mut bl_count = vec![0u32; max_bits + 1];
for &l in lengths {
if l > 0 {
bl_count[l as usize] += 1;
}
}
let mut next_code = vec![0u32; max_bits + 2];
let mut code = 0u32;
for bits in 1..=max_bits {
code = (code + bl_count[bits - 1]) << 1;
next_code[bits] = code;
}
let table_size = 1usize << max_bits;
let mut table: Vec<(u16, u8)> = vec![(0xffff, 0); table_size];
for (sym, &len) in lengths.iter().enumerate() {
if len == 0 {
continue;
}
let c = next_code[len as usize];
next_code[len as usize] += 1;
let rev = reverse_bits(c, len);
let step = 1usize << len;
let mut idx = rev as usize;
while idx < table_size {
table[idx] = (sym as u16, len);
idx += step;
}
}
Some((table, max_bits as u8))
}
fn reverse_bits(v: u32, n: u8) -> u32 {
let mut r = 0u32;
let mut v = v;
for _ in 0..n {
r = (r << 1) | (v & 1);
v >>= 1;
}
r
}
fn huffman_decode(br: &mut BitReader, table: &[(u16, u8)], max_bits: u8) -> Option<u16> {
br.fill();
if max_bits == 0 {
return None;
}
let peeked = br.bits & ((1 << max_bits) - 1);
let (sym, len) = table[peeked as usize];
if len == 0 || sym == 0xffff {
return None;
}
br.bits >>= len;
br.nbits -= len as u32;
Some(sym)
}
fn fixed_literal_lengths() -> Vec<u8> {
let mut v = Vec::with_capacity(288);
for i in 0u16..288 {
let l = if i < 144 {
8
} else if i < 256 {
9
} else if i < 280 {
7
} else {
8
};
v.push(l);
}
v
}
fn fixed_distance_lengths() -> Vec<u8> {
vec![5u8; 32]
}
fn decode_length(code: u16, br: &mut BitReader) -> Option<u16> {
if code < 257 {
return None;
}
if code == 285 {
return Some(258);
}
let extra_bits_table = [
0u32, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
];
let base_table = [
3u16, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99,
115, 131, 163, 195, 227, 258,
];
let idx = (code - 257) as usize;
if idx >= 29 {
return None;
}
let extra = br.read_bits(extra_bits_table[idx])?;
Some(base_table[idx] + extra as u16)
}
fn decode_distance(code: u16, br: &mut BitReader) -> Option<u32> {
if code >= 30 {
return None;
}
let extra_bits_table = [
0u32, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12,
12, 13, 13,
];
let base_table = [
1u32, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025,
1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577,
];
let extra = br.read_bits(extra_bits_table[code as usize])?;
Some(base_table[code as usize] + extra)
}
fn inflate_block(
br: &mut BitReader,
lit_table: &[(u16, u8)],
lit_max: u8,
dist_table: &[(u16, u8)],
dist_max: u8,
out: &mut Vec<u8>,
) -> Option<bool> {
loop {
let sym = huffman_decode(br, lit_table, lit_max)?;
if sym < 256 {
out.push(sym as u8);
} else if sym == 256 {
return Some(false); } else {
let length = decode_length(sym, br)? as usize;
let dist_code = huffman_decode(br, dist_table, dist_max)?;
let dist = decode_distance(dist_code, br)? as usize;
if dist > out.len() {
return None;
}
let start = out.len() - dist;
for i in 0..length {
let b = out[start + i % dist];
out.push(b);
}
}
}
}
fn deflate_decompress(data: &[u8]) -> Option<Vec<u8>> {
let mut br = BitReader::new(data);
let mut out = Vec::new();
loop {
let bfinal = br.read_bits(1)?;
let btype = br.read_bits(2)?;
match btype {
0 => {
br.align_byte();
let len = br.read_aligned_u16_le()? as usize;
let nlen = br.read_aligned_u16_le()? as usize;
if (len ^ nlen) & 0xffff != 0xffff {
return None;
}
for _ in 0..len {
out.push(br.read_aligned_byte()?);
}
}
1 => {
let lit_lens = fixed_literal_lengths();
let dist_lens = fixed_distance_lengths();
let (lit_table, lit_max) = build_huffman(&lit_lens)?;
let (dist_table, dist_max) = build_huffman(&dist_lens)?;
inflate_block(
&mut br,
&lit_table,
lit_max,
&dist_table,
dist_max,
&mut out,
)?;
}
2 => {
let hlit = br.read_bits(5)? as usize + 257;
let hdist = br.read_bits(5)? as usize + 1;
let hclen = br.read_bits(4)? as usize + 4;
let cl_order = [
16usize, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
let mut cl_lengths = vec![0u8; 19];
for i in 0..hclen {
cl_lengths[cl_order[i]] = br.read_bits(3)? as u8;
}
let (cl_table, cl_max) = build_huffman(&cl_lengths)?;
let total = hlit + hdist;
let mut all_lengths = Vec::with_capacity(total);
while all_lengths.len() < total {
let code = huffman_decode(&mut br, &cl_table, cl_max)?;
match code {
0..=15 => all_lengths.push(code as u8),
16 => {
let extra = br.read_bits(2)? as usize + 3;
let last = *all_lengths.last()?;
for _ in 0..extra {
all_lengths.push(last);
}
}
17 => {
let extra = br.read_bits(3)? as usize + 3;
all_lengths.resize(all_lengths.len() + extra, 0);
}
18 => {
let extra = br.read_bits(7)? as usize + 11;
all_lengths.resize(all_lengths.len() + extra, 0);
}
_ => return None,
}
}
let lit_lens = &all_lengths[..hlit];
let dist_lens = &all_lengths[hlit..hlit + hdist];
let (lit_table, lit_max) = build_huffman(lit_lens)?;
let (dist_table, dist_max) = build_huffman(dist_lens)?;
inflate_block(
&mut br,
&lit_table,
lit_max,
&dist_table,
dist_max,
&mut out,
)?;
}
_ => return None,
}
if bfinal == 1 {
break;
}
}
Some(out)
}
fn gunzip(data: &[u8]) -> Option<Vec<u8>> {
if data.len() < 18 {
return None;
}
if data[0] != 0x1f || data[1] != 0x8b {
return None;
}
let method = data[2];
if method != 8 {
return None;
}
let flags = data[3];
let mut pos = 10usize;
if flags & 0x04 != 0 {
if pos + 2 > data.len() {
return None;
}
let xlen = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2 + xlen;
}
if flags & 0x08 != 0 {
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
pos += 1;
}
if flags & 0x10 != 0 {
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
pos += 1;
}
if flags & 0x02 != 0 {
pos += 2;
}
if pos + 8 > data.len() {
return None;
}
let compressed = &data[pos..data.len() - 8];
deflate_decompress(compressed)
}
fn hdrp_decrypt_gunzip(data: &[u8]) -> Option<Vec<u8>> {
let decrypted = hdrp_decrypt(data)?;
gunzip(&decrypted)
}
fn read_varint(data: &[u8], pos: usize) -> Option<(u64, usize)> {
let mut val = 0u64;
let mut shift = 0u32;
let mut p = pos;
loop {
if p >= data.len() {
return None;
}
let b = data[p] as u64;
p += 1;
val |= (b & 0x7f) << shift;
if b & 0x80 == 0 {
break;
}
shift += 7;
if shift >= 70 {
return None;
} }
Some((val, p))
}
fn read_len_delimited(data: &[u8], pos: usize) -> Option<(&[u8], usize)> {
let (len, p) = read_varint(data, pos)?;
let end = p + len as usize;
if end > data.len() {
return None;
}
Some((&data[p..end], end))
}
struct FieldDef {
path: &'static str, name: &'static str,
fmt: FieldFmt,
group2: &'static str,
}
#[derive(Clone, Copy, Debug)]
enum FieldFmt {
String,
Binary,
Unsigned, Float, FloatDiv1000, UnixTimeMs, }
static HDRP_FIELDS: &[FieldDef] = &[
FieldDef {
path: "1-1",
name: "ImageName",
fmt: FieldFmt::String,
group2: "Image",
},
FieldDef {
path: "1-2",
name: "ImageData",
fmt: FieldFmt::Binary,
group2: "Image",
},
FieldDef {
path: "2",
name: "TimeLogText",
fmt: FieldFmt::Binary,
group2: "Image",
},
FieldDef {
path: "3",
name: "SummaryText",
fmt: FieldFmt::Binary,
group2: "Image",
},
FieldDef {
path: "9-3",
name: "FrameCount",
fmt: FieldFmt::Unsigned,
group2: "Image",
},
FieldDef {
path: "12-1",
name: "DeviceMake",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-2",
name: "DeviceModel",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-3",
name: "DeviceCodename",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-4",
name: "DeviceHardwareRevision",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-6",
name: "HDRPSoftware",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-7",
name: "AndroidRelease",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-8",
name: "SoftwareDate",
fmt: FieldFmt::UnixTimeMs,
group2: "Time",
},
FieldDef {
path: "12-9",
name: "Application",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-10",
name: "AppVersion",
fmt: FieldFmt::String,
group2: "Device",
},
FieldDef {
path: "12-12-1",
name: "ExposureTimeMin",
fmt: FieldFmt::FloatDiv1000,
group2: "Camera",
},
FieldDef {
path: "12-12-2",
name: "ExposureTimeMax",
fmt: FieldFmt::FloatDiv1000,
group2: "Camera",
},
FieldDef {
path: "12-13-1",
name: "ISOMin",
fmt: FieldFmt::Float,
group2: "Camera",
},
FieldDef {
path: "12-13-2",
name: "ISOMax",
fmt: FieldFmt::Float,
group2: "Camera",
},
FieldDef {
path: "12-14",
name: "MaxAnalogISO",
fmt: FieldFmt::Float,
group2: "Camera",
},
];
fn make_tag(name: &str, group2: &str, raw: Value, print: String) -> Tag {
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: name.to_string(),
group: TagGroup {
family0: "MakerNotes".into(),
family1: "MakerNotes".into(),
family2: group2.into(),
},
raw_value: raw,
print_value: print,
priority: 0,
}
}
fn parse_protobuf(data: &[u8], prefix: &str, tags: &mut Vec<Tag>) {
let mut pos = 0;
while pos < data.len() {
let (tag_word, p) = match read_varint(data, pos) {
Some(v) => v,
None => break,
};
let field_id = tag_word >> 3;
let wire_type = tag_word & 7;
pos = p;
let path = if prefix.is_empty() {
format!("{}", field_id)
} else {
format!("{}{}", prefix, field_id)
};
match wire_type {
0 => {
let (val, p2) = match read_varint(data, pos) {
Some(v) => v,
None => break,
};
pos = p2;
if let Some(def) = HDRP_FIELDS.iter().find(|d| d.path == path) {
let (raw, print) = match def.fmt {
FieldFmt::Unsigned => {
let s = format!("{}", val);
(Value::U32(val as u32), s)
}
FieldFmt::UnixTimeMs => {
let secs = (val / 1000) as i64;
let ms = val % 1000;
let s = format_unix_time_ms(secs, ms as u32);
(Value::String(s.clone()), s)
}
_ => {
let s = format!("{}", val);
(Value::U32(val as u32), s)
}
};
tags.push(make_tag(def.name, def.group2, raw, print));
}
}
1 => {
if pos + 8 > data.len() {
break;
}
pos += 8;
}
2 => {
let (bytes, p2) = match read_len_delimited(data, pos) {
Some(v) => v,
None => break,
};
pos = p2;
if let Some(def) = HDRP_FIELDS.iter().find(|d| d.path == path) {
match def.fmt {
FieldFmt::String => {
let s = crate::encoding::decode_utf8_or_latin1(bytes).to_string();
tags.push(make_tag(def.name, def.group2, Value::String(s.clone()), s));
}
FieldFmt::Binary => {
let print = format!(
"(Binary data {} bytes, use -b option to extract)",
bytes.len()
);
tags.push(make_tag(
def.name,
def.group2,
Value::Binary(bytes.to_vec()),
print,
));
}
FieldFmt::Float | FieldFmt::FloatDiv1000 => {
if bytes.len() >= 4 {
let f =
f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let v = if matches!(def.fmt, FieldFmt::FloatDiv1000) {
f as f64 / 1000.0
} else {
f as f64
};
let s = format_float(v);
tags.push(make_tag(def.name, def.group2, Value::F64(v), s));
}
}
_ => {
parse_protobuf(bytes, &format!("{}-", path), tags);
}
}
} else {
parse_protobuf(bytes, &format!("{}-", path), tags);
}
}
5 => {
if pos + 4 > data.len() {
break;
}
let bytes = &data[pos..pos + 4];
pos += 4;
if let Some(def) = HDRP_FIELDS.iter().find(|d| d.path == path) {
if matches!(def.fmt, FieldFmt::Float | FieldFmt::FloatDiv1000) {
let f = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let v = if matches!(def.fmt, FieldFmt::FloatDiv1000) {
f as f64 / 1000.0
} else {
f as f64
};
let s = format_float(v);
tags.push(make_tag(def.name, def.group2, Value::F64(v), s));
}
}
}
3 | 4 => {
}
_ => break, }
}
}
fn parse_hdrp_protobuf(data: &[u8]) -> Vec<Tag> {
let mut tags = Vec::new();
parse_protobuf(data, "", &mut tags);
tags
}
fn format_float(v: f64) -> String {
if v == 0.0 {
return "0".to_string();
}
format_g(v, 15)
}
fn format_g(v: f64, prec: usize) -> String {
if v == 0.0 {
return "0".to_string();
}
let abs = v.abs();
let exp = abs.log10().floor() as i32;
if exp >= prec as i32 || exp < -4 {
let _mantissa = v / 10f64.powi(exp);
let decimals = prec.saturating_sub(1);
let s = format!("{:.prec$e}", v, prec = decimals);
normalize_sci(s)
} else {
let decimals = if exp >= 0 {
prec.saturating_sub(exp as usize + 1)
} else {
prec + (-exp - 1) as usize
};
let s = format!("{:.prec$}", v, prec = decimals);
strip_trailing_zeros(s)
}
}
fn normalize_sci(s: String) -> String {
if let Some(e_pos) = s.find('e') {
let mantissa = &s[..e_pos];
let exp_str = &s[e_pos + 1..];
let mantissa = strip_trailing_zeros(mantissa.to_string());
let exp: i32 = exp_str.parse().unwrap_or(0);
if exp >= 0 {
format!("{}e+{:02}", mantissa, exp)
} else {
format!("{}e-{:02}", mantissa, -exp)
}
} else {
s
}
}
fn strip_trailing_zeros(s: String) -> String {
if s.contains('.') {
let s = s.trim_end_matches('0').to_string();
let s = s.trim_end_matches('.').to_string();
s
} else {
s
}
}
fn format_unix_time_ms(secs: i64, ms: u32) -> String {
let (year, month, day, hour, min, sec) = unix_to_datetime(secs);
format!(
"{:04}:{:02}:{:02} {:02}:{:02}:{:02}.{:03}+00:00",
year, month, day, hour, min, sec, ms
)
}
fn unix_to_datetime(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
let sec = (secs % 60) as u32;
let min_total = secs / 60;
let min = (min_total % 60) as u32;
let hour_total = min_total / 60;
let hour = (hour_total % 24) as u32;
let days = hour_total / 24;
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as i32, m as u32, d as u32, hour, min, sec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base64_decode() {
let encoded = "SERS"; let decoded = base64_decode(encoded);
assert!(decoded.is_some());
}
#[test]
fn test_reverse_bits() {
assert_eq!(reverse_bits(0b101, 3), 0b101);
assert_eq!(reverse_bits(0b001, 3), 0b100);
assert_eq!(reverse_bits(0b110, 3), 0b011);
}
#[test]
fn test_deflate_stored() {
let data = b"Hello";
let len = data.len() as u16;
let nlen = !len;
let mut block = vec![0x01u8]; block.extend_from_slice(&len.to_le_bytes());
block.extend_from_slice(&nlen.to_le_bytes());
block.extend_from_slice(data);
let result = deflate_decompress(&block);
assert_eq!(result, Some(b"Hello".to_vec()));
}
}
#[cfg(test)]
mod actual_data_tests {
use super::*;
#[test]
fn test_actual_hdrp_decode() {
let b64_path = "/tmp/hdrp_b64.txt";
if let Ok(b64) = std::fs::read_to_string(b64_path) {
let b64 = b64.trim();
let raw = base64_decode(b64).expect("b64 decode failed");
assert!(raw.len() > 5, "too short: {}", raw.len());
assert_eq!(&raw[..4], b"HDRP", "not HDRP: {:?}", &raw[..4]);
assert_eq!(raw[4], 3, "wrong version: {}", raw[4]);
let decrypted = hdrp_decrypt(&raw).expect("decrypt failed");
assert!(decrypted.len() > 2, "decrypt too short");
assert_eq!(
decrypted[0], 0x1f,
"not gzip magic[0]: {:02x}",
decrypted[0]
);
assert_eq!(
decrypted[1], 0x8b,
"not gzip magic[1]: {:02x}",
decrypted[1]
);
let decompressed = gunzip(&decrypted).expect("gunzip failed");
assert!(!decompressed.is_empty(), "decompressed is empty");
eprintln!(
"decompressed {} bytes, first 20: {}",
decompressed.len(),
decompressed
.iter()
.take(20)
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(" ")
);
let tags = parse_hdrp_protobuf(&decompressed);
eprintln!("Found {} tags", tags.len());
for tag in &tags {
eprintln!(" {} = {}", tag.name, tag.print_value);
}
assert!(!tags.is_empty(), "no tags found");
} else {
eprintln!("Test file not found, skipping");
}
}
}