#[must_use]
pub fn decode_dchars(data: &[u8]) -> String {
let Some(&comp_id) = data.first() else {
return String::new();
};
let body = data.get(1..).unwrap_or_default();
match comp_id {
8 => body.iter().map(|&b| char::from(b)).collect(),
16 => {
let units = body.chunks_exact(2).map(|pair| {
let hi = pair.first().copied().unwrap_or(0);
let lo = pair.get(1).copied().unwrap_or(0);
u16::from_be_bytes([hi, lo])
});
char::decode_utf16(units).map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)).collect()
}
_ => String::new(),
}
}
#[must_use]
pub fn decode_dstring(field: &[u8]) -> String {
let Some(&used_len) = field.last() else {
return String::new();
};
let used = usize::from(used_len);
field.get(..used).map_or_else(String::new, decode_dchars)
}
#[cfg(test)]
mod tests {
use proptest::prelude::{any, prop_assert, prop_assert_eq, proptest};
use super::{decode_dchars, decode_dstring};
#[test]
fn dchars_compression_8_is_latin1() {
let data = [8_u8, b'B', b'D', b'M', b'V'];
assert_eq!(decode_dchars(&data), "BDMV");
}
#[test]
fn dchars_compression_8_decodes_high_bytes_as_latin1() {
let data = [8_u8, 0xE9];
assert_eq!(decode_dchars(&data), "é");
}
#[test]
fn dchars_compression_16_is_utf16_be() {
let data = [16_u8, 0x00, 0x41, 0x00, 0xE9];
assert_eq!(decode_dchars(&data), "Aé");
}
#[test]
fn dchars_compression_16_joins_surrogate_pairs() {
let data = [16_u8, 0xD8, 0x3D, 0xDE, 0x00];
assert_eq!(decode_dchars(&data), "😀");
}
#[test]
fn dchars_compression_16_lone_surrogate_is_replacement() {
let data = [16_u8, 0xD8, 0x3D];
assert_eq!(decode_dchars(&data), "\u{FFFD}");
}
#[test]
fn dchars_compression_16_drops_odd_trailing_byte() {
let data = [16_u8, 0x00, 0x41, 0x00];
assert_eq!(decode_dchars(&data), "A");
}
#[test]
fn dchars_empty_and_id_only_are_empty() {
assert_eq!(decode_dchars(&[]), "");
assert_eq!(decode_dchars(&[8]), "");
assert_eq!(decode_dchars(&[16]), "");
}
#[test]
fn dchars_unknown_compression_is_empty() {
assert_eq!(decode_dchars(&[0, b'x', b'y']), "");
assert_eq!(decode_dchars(&[255, 1, 2, 3]), "");
}
#[test]
fn dstring_uses_trailing_length_byte() {
let field = [8_u8, b'P', b'L', b'A', b'Y', b'L', b'I', b'S', b'T', 0, 0, 9];
assert_eq!(decode_dstring(&field), "PLAYLIST");
let shorter = [8_u8, b'P', b'L', b'A', b'Y', b'L', b'I', b'S', b'T', 0, 0, 7];
assert_eq!(decode_dstring(&shorter), "PLAYLI");
}
#[test]
fn dstring_zero_used_is_empty() {
let field = [8_u8, b'X', b'Y', 0];
assert_eq!(decode_dstring(&field), "");
}
#[test]
fn dstring_empty_or_overlong_used_is_empty() {
assert_eq!(decode_dstring(&[]), "");
assert_eq!(decode_dstring(&[8, b'A', b'B', 200]), "");
}
proptest! {
#[test]
fn dchars_never_panics(data in any::<Vec<u8>>()) {
let s = decode_dchars(&data);
if data.first() == Some(&8) {
prop_assert_eq!(s.chars().count(), data.len().saturating_sub(1));
}
prop_assert!(s.chars().count() <= data.len());
}
#[test]
fn dstring_never_panics(field in any::<Vec<u8>>()) {
let s = decode_dstring(&field);
prop_assert!(s.chars().count() <= field.len());
}
#[test]
fn dstring_equals_dchars_of_used_prefix(body in any::<Vec<u8>>(), used in 0_usize..40) {
let used = used.min(body.len());
let mut field = body.clone();
field.push(u8::try_from(used).unwrap_or(0));
let expected = body.get(..used).map(decode_dchars).unwrap_or_default();
prop_assert_eq!(decode_dstring(&field), expected);
}
}
}