use std::io::{Cursor, Read};
use std::path::Path;
use uuid::Uuid;
use crate::error::{Error, Result};
use crate::ole::clsid;
pub struct OleFile {
inner: cfb::CompoundFile<Cursor<Vec<u8>>>,
}
#[derive(Debug, Clone)]
pub struct OleEntryWithTimestamp {
pub path: String,
pub is_stream: bool,
pub size: u64,
pub clsid: Uuid,
pub created: Option<std::time::SystemTime>,
pub modified: Option<std::time::SystemTime>,
}
#[derive(Debug, Clone)]
pub struct OleEntry {
pub path: String,
pub is_stream: bool,
pub size: u64,
pub clsid: Uuid,
}
impl OleFile {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let data = std::fs::read(path)?;
Self::from_bytes(&data)
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let cursor = Cursor::new(data.to_vec());
let inner = cfb::CompoundFile::open(cursor)
.map_err(|e| Error::InvalidOle(format!("Failed to open OLE file: {e}")))?;
Ok(Self { inner })
}
pub fn from_reader<R: Read>(mut reader: R) -> Result<Self> {
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
Self::from_bytes(&data)
}
pub fn is_ole(data: &[u8]) -> bool {
data.len() >= 8 && data[0..8] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
}
pub fn list_entries(&self) -> Vec<OleEntry> {
let mut entries = Vec::new();
let root = Path::new("/");
Self::collect_entries(&self.inner, root, &mut entries);
entries
}
pub fn list_streams(&self) -> Vec<String> {
self.list_entries()
.into_iter()
.filter(|e| e.is_stream)
.map(|e| e.path)
.collect()
}
pub fn exists(&self, path: &str) -> bool {
self.inner.is_stream(path)
|| self.inner.is_storage(path)
}
pub fn is_stream(&self, path: &str) -> bool {
self.inner.is_stream(path)
}
pub fn open_stream(&mut self, path: &str) -> Result<Vec<u8>> {
let mut stream = self.inner.open_stream(path).map_err(|e| {
Error::InvalidOle(format!("Failed to open stream '{path}': {e}"))
})?;
let mut buf = Vec::new();
stream.read_to_end(&mut buf)?;
Ok(buf)
}
pub fn root_clsid(&self) -> Uuid {
let root = self.inner.root_entry();
root.clsid().to_owned()
}
pub fn root_clsid_description(&self) -> Option<&'static str> {
clsid::lookup_clsid(&self.root_clsid())
}
pub fn entry_clsid(&self, path: &str) -> Option<Uuid> {
self.inner.entry(path).ok().map(|e| e.clsid().to_owned())
}
pub fn list_entries_with_timestamps(&self) -> Vec<OleEntryWithTimestamp> {
let mut entries = Vec::new();
let root = Path::new("/");
Self::collect_entries_with_timestamps(&self.inner, root, &mut entries);
entries
}
fn collect_entries_with_timestamps(
cf: &cfb::CompoundFile<Cursor<Vec<u8>>>,
dir: &Path,
entries: &mut Vec<OleEntryWithTimestamp>,
) {
if let Ok(iter) = cf.read_storage(dir) {
let children: Vec<_> = iter.collect();
for entry in children {
let path_str = entry.path().to_string_lossy().to_string();
let is_stream = entry.is_stream();
let size = entry.len();
let clsid = entry.clsid().to_owned();
let created = Some(entry.created());
let modified = Some(entry.modified());
entries.push(OleEntryWithTimestamp {
path: path_str.clone(),
is_stream,
size,
clsid,
created,
modified,
});
if !is_stream {
Self::collect_entries_with_timestamps(cf, Path::new(&path_str), entries);
}
}
}
}
fn collect_entries(
cf: &cfb::CompoundFile<Cursor<Vec<u8>>>,
dir: &Path,
entries: &mut Vec<OleEntry>,
) {
if let Ok(iter) = cf.read_storage(dir) {
let children: Vec<_> = iter.collect();
for entry in children {
let path_str = entry.path().to_string_lossy().to_string();
let is_stream = entry.is_stream();
let size = entry.len();
let clsid = entry.clsid().to_owned();
entries.push(OleEntry {
path: path_str.clone(),
is_stream,
size,
clsid,
});
if !is_stream {
Self::collect_entries(cf, Path::new(&path_str), entries);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_ole_magic() {
let ole_header = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
assert!(OleFile::is_ole(&ole_header));
assert!(!OleFile::is_ole(&[0x50, 0x4B, 0x03, 0x04])); assert!(!OleFile::is_ole(&[0x00, 0x01]));
}
#[test]
fn test_invalid_ole_data() {
let result = OleFile::from_bytes(&[0x00, 0x01, 0x02, 0x03]);
assert!(result.is_err());
}
#[test]
fn test_empty_data() {
let result = OleFile::from_bytes(&[]);
assert!(result.is_err());
}
}