use flate2::read::ZlibDecoder;
use std::io::Read;
use crate::container;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamKind {
Partition,
Deltas,
Plain,
Preview,
}
impl StreamKind {
pub fn label(self) -> &'static str {
match self {
StreamKind::Partition => "partition",
StreamKind::Deltas => "deltas",
StreamKind::Plain => "plain",
StreamKind::Preview => "preview",
}
}
pub fn is_parasolid(self) -> bool {
!matches!(self, StreamKind::Preview)
}
}
#[derive(Debug, Clone)]
pub struct Stream {
pub file_offset: usize,
pub inflated: Vec<u8>,
pub kind: StreamKind,
pub schema: Option<String>,
}
const MIN_INFLATED: usize = 64;
pub fn extract_streams(data: &[u8]) -> Vec<Stream> {
let Ok(container) = container::scan_bytes(data.to_vec()) else {
return Vec::new();
};
let Some((part_offset, part_size)) = container
.entries
.iter()
.find(|entry| entry.name == "/Root/UG_PART/UG_PART")
.and_then(|entry| entry.file_span)
else {
return Vec::new();
};
let Ok(start) = usize::try_from(part_offset) else {
return Vec::new();
};
let Ok(size) = usize::try_from(part_size) else {
return Vec::new();
};
let Some(part) = data.get(start..start.saturating_add(size)) else {
return Vec::new();
};
let mut streams = Vec::new();
let mut i = 0usize;
while i + 2 <= part.len() {
if is_zlib_header(part[i], part[i + 1]) {
if let Some(inflated) = inflate(&part[i..]) {
if inflated.len() >= MIN_INFLATED {
let (kind, schema) = classify(&inflated);
streams.push(Stream {
file_offset: start + i,
inflated,
kind,
schema,
});
i += 2;
continue;
}
}
}
i += 1;
}
streams
}
#[allow(clippy::manual_is_multiple_of)] fn is_zlib_header(cmf: u8, flg: u8) -> bool {
cmf & 0x0f == 8 && cmf >> 4 <= 7 && u16::from_be_bytes([cmf, flg]).is_multiple_of(31)
}
fn inflate(bytes: &[u8]) -> Option<Vec<u8>> {
let mut dec = ZlibDecoder::new(bytes);
let mut out = Vec::new();
match dec.read_to_end(&mut out) {
Ok(_) => Some(out),
Err(_) if !out.is_empty() => Some(out),
Err(_) => None,
}
}
fn classify(inflated: &[u8]) -> (StreamKind, Option<String>) {
if !inflated.starts_with(b"PS\x00\x00") {
return (StreamKind::Preview, None);
}
let window = &inflated[..inflated.len().min(512)];
let kind = if contains(window, b"(partition)") {
StreamKind::Partition
} else if contains(window, b"(deltas)") {
StreamKind::Deltas
} else {
StreamKind::Plain
};
(kind, read_schema(window))
}
fn read_schema(window: &[u8]) -> Option<String> {
let pos = find(window, b"SCH_")?;
let mut end = pos;
while end < window.len() && (window[end].is_ascii_alphanumeric() || window[end] == b'_') {
end += 1;
}
Some(String::from_utf8_lossy(&window[pos..end]).into_owned())
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
find(haystack, needle).is_some()
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}