use std::io::Read;
use crate::error::XmltvError;
const GZIP_MAGIC: [u8; 2] = [0x1F, 0x8B];
const XZ_MAGIC: [u8; 6] = [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00];
pub fn is_gzip(data: &[u8]) -> bool {
data.len() >= 2 && data[..2] == GZIP_MAGIC
}
pub fn is_xz(data: &[u8]) -> bool {
data.len() >= 6 && data[..6] == XZ_MAGIC
}
pub fn decompress_auto(data: &[u8]) -> Result<Vec<u8>, XmltvError> {
if is_gzip(data) {
decompress_gzip(data)
} else if is_xz(data) {
decompress_xz(data)
} else {
Ok(data.to_vec())
}
}
pub fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>, XmltvError> {
if data.is_empty() {
return Ok(Vec::new());
}
let mut decoder = flate2::read::GzDecoder::new(data);
let mut decompressed = Vec::with_capacity(data.len() * 2);
decoder
.read_to_end(&mut decompressed)
.map_err(|e| XmltvError::Decompression(format!("gzip decompression failed: {e}")))?;
Ok(decompressed)
}
pub fn decompress_xz(data: &[u8]) -> Result<Vec<u8>, XmltvError> {
if data.is_empty() {
return Ok(Vec::new());
}
let mut decoder = xz2::read::XzDecoder::new(data);
let mut decompressed = Vec::with_capacity(data.len() * 4);
decoder
.read_to_end(&mut decompressed)
.map_err(|e| XmltvError::Decompression(format!("XZ decompression failed: {e}")))?;
Ok(decompressed)
}
#[cfg(test)]
mod tests {
use super::*;
fn gzip_compress(data: &[u8]) -> Vec<u8> {
use flate2::write::GzEncoder;
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::fast());
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
}
fn xz_compress(data: &[u8]) -> Vec<u8> {
use std::io::Write;
use xz2::write::XzEncoder;
let mut encoder = XzEncoder::new(Vec::new(), 1);
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
}
#[test]
fn is_gzip_detects_magic_bytes() {
let compressed = gzip_compress(b"hello");
assert!(is_gzip(&compressed));
}
#[test]
fn is_gzip_rejects_plain_text() {
assert!(!is_gzip(b"plain text"));
}
#[test]
fn is_gzip_rejects_short_input() {
assert!(!is_gzip(&[0x1F]));
assert!(!is_gzip(&[]));
}
#[test]
fn is_xz_detects_magic_bytes() {
let compressed = xz_compress(b"hello");
assert!(is_xz(&compressed));
}
#[test]
fn is_xz_rejects_plain_text() {
assert!(!is_xz(b"plain text"));
}
#[test]
fn is_xz_rejects_short_input() {
assert!(!is_xz(&[0xFD, 0x37, 0x7A]));
assert!(!is_xz(&[]));
}
#[test]
fn decompress_gzip_roundtrip() {
let original = b"<?xml version=\"1.0\"?><tv><channel id=\"ch1\"><display-name>Test</display-name></channel></tv>";
let compressed = gzip_compress(original);
let decompressed = decompress_gzip(&compressed).unwrap();
assert_eq!(decompressed, original);
}
#[test]
fn decompress_xz_roundtrip() {
let original = b"<?xml version=\"1.0\"?><tv><channel id=\"ch1\"><display-name>Test</display-name></channel></tv>";
let compressed = xz_compress(original);
let decompressed = decompress_xz(&compressed).unwrap();
assert_eq!(decompressed, original);
}
#[test]
fn decompress_auto_detects_gzip() {
let original = b"hello gzip world";
let compressed = gzip_compress(original);
let result = decompress_auto(&compressed).unwrap();
assert_eq!(result, original);
}
#[test]
fn decompress_auto_detects_xz() {
let original = b"hello xz world";
let compressed = xz_compress(original);
let result = decompress_auto(&compressed).unwrap();
assert_eq!(result, original);
}
#[test]
fn decompress_auto_passthrough_uncompressed() {
let plain = b"<?xml version=\"1.0\"?><tv></tv>";
let result = decompress_auto(plain).unwrap();
assert_eq!(result, plain);
}
#[test]
fn decompress_gzip_empty_input() {
let result = decompress_gzip(&[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn decompress_xz_empty_input() {
let result = decompress_xz(&[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn decompress_gzip_invalid_data_returns_error() {
let bad = [0x1F, 0x8B, 0x08, 0x00, 0xFF, 0xFF];
let result = decompress_gzip(&bad);
assert!(result.is_err());
}
#[test]
fn end_to_end_compressed_gzip_parse() {
let xmltv = r#"<?xml version="1.0" encoding="UTF-8"?>
<tv>
<channel id="ch1">
<display-name>Test Channel</display-name>
</channel>
<programme start="20250115120000 +0000" stop="20250115130000 +0000" channel="ch1">
<title>Test Show</title>
</programme>
</tv>"#;
let compressed = gzip_compress(xmltv.as_bytes());
let doc = crate::parse_compressed(&compressed).unwrap();
assert_eq!(doc.channels.len(), 1);
assert_eq!(doc.channels[0].id, "ch1");
assert_eq!(doc.programmes.len(), 1);
assert_eq!(doc.programmes[0].title[0].value, "Test Show");
}
}