use std::io::{Cursor, Read};
use std::path::Path;
use cfb::CompoundFile;
use crate::error::{Error, Result};
use crate::ppt97::crypto::encrypt_ppt_stream;
use crate::ppt97::ole::{fix_mini_fat, write_stream};
use crate::ppt97::record::{
parse_persist_directory, parse_record_header, read_u32_le, write_u32_le, RT_CURRENT_USER_ATOM,
RT_USER_EDIT_ATOM,
};
use crate::ppt97::watermark::inject_watermark;
pub mod crypto;
pub mod ole;
pub mod record;
pub mod watermark;
pub use watermark::WatermarkConfig;
fn read_all_streams(comp: &mut CompoundFile<Cursor<Vec<u8>>>) -> Result<Vec<(String, Vec<u8>)>> {
let mut stream_paths: Vec<String> = Vec::new();
for entry in comp.walk() {
if entry.is_root() || entry.is_storage() {
continue;
}
if entry.is_stream() {
stream_paths.push(entry.path().to_string_lossy().to_string());
}
}
let mut streams = Vec::with_capacity(stream_paths.len());
for path in &stream_paths {
let mut stream = comp
.open_stream(path)
.map_err(|e| Error::ppt97(format!("read_all_streams: open {} failed: {}", path, e)))?;
let mut data = Vec::new();
stream
.read_to_end(&mut data)
.map_err(|e| Error::ppt97(format!("read_all_streams: read {} failed: {}", path, e)))?;
streams.push((path.clone(), data));
}
Ok(streams)
}
#[allow(clippy::type_complexity)]
fn classify_streams(
streams: Vec<(String, Vec<u8>)>,
) -> (Option<Vec<u8>>, Option<Vec<u8>>, Vec<(String, Vec<u8>)>) {
let mut cu_data: Option<Vec<u8>> = None;
let mut ppt_data: Option<Vec<u8>> = None;
let mut other_streams: Vec<(String, Vec<u8>)> = Vec::new();
for (path, data) in streams {
let name = path.strip_prefix('/').unwrap_or(&path);
match name {
"Current User" => cu_data = Some(data),
"PowerPoint Document" => ppt_data = Some(data),
_ => other_streams.push((path, data)),
}
}
(cu_data, ppt_data, other_streams)
}
fn write_back_streams(
mut comp: CompoundFile<Cursor<Vec<u8>>>,
cu_data: &[u8],
ppt_data: &[u8],
other_streams: &[(String, Vec<u8>)],
) -> Result<Vec<u8>> {
write_stream(&mut comp, "Current User", cu_data)?;
write_stream(&mut comp, "PowerPoint Document", ppt_data)?;
for (path, data) in other_streams {
write_stream(&mut comp, path, data)?;
}
comp.flush()
.map_err(|e| Error::ppt97(format!("write_back_streams: flush failed: {}", e)))?;
let cursor = comp.into_inner();
let mut data = cursor.into_inner();
fix_mini_fat(&mut data)?;
Ok(data)
}
pub fn add_watermark(input_path: &Path, config: &WatermarkConfig) -> Result<Vec<u8>> {
let file_data = std::fs::read(input_path)?;
let cursor = Cursor::new(file_data);
let mut comp = CompoundFile::open(cursor)
.map_err(|e| Error::ppt97(format!("add_watermark: open OLE2 failed: {}", e)))?;
let streams = read_all_streams(&mut comp)?;
let (cu_data, ppt_data, other_streams) = classify_streams(streams);
let mut ppt_data = ppt_data
.ok_or_else(|| Error::ppt97("add_watermark: PowerPoint Document stream not found"))?;
let mut cu_data =
cu_data.ok_or_else(|| Error::ppt97("add_watermark: Current User stream not found"))?;
let (_, _, cu_type, _) = parse_record_header(&cu_data, 0)?;
if cu_type != RT_CURRENT_USER_ATOM {
return Err(Error::ppt97(format!(
"add_watermark: expected CurrentUserAtom (0x{:04X}), got 0x{:04X}",
RT_CURRENT_USER_ATOM, cu_type
)));
}
let ue_offset_old = read_u32_le(&cu_data, 16)? as usize;
let (_, _, ue_type, ue_len) = parse_record_header(&ppt_data, ue_offset_old)?;
if ue_type != RT_USER_EDIT_ATOM {
return Err(Error::ppt97(format!(
"add_watermark: expected UserEditAtom (0x{:04X}), got 0x{:04X}",
RT_USER_EDIT_ATOM, ue_type
)));
}
if ue_len != 28 {
return Err(Error::ppt97(format!(
"add_watermark: file already encrypted or malformed (UserEditAtom.recLen={}, expected 28)",
ue_len
)));
}
let pd_offset_old = read_u32_le(&ppt_data, ue_offset_old + 20)? as usize;
let persist_entries = parse_persist_directory(&ppt_data, pd_offset_old)?;
let (total_inserted, _new_entries, pd_offset_new) =
inject_watermark(&mut ppt_data, config, &persist_entries, pd_offset_old)?;
let ue_offset_new = ue_offset_old + total_inserted;
write_u32_le(&mut ppt_data, ue_offset_new + 20, pd_offset_new as u32)?;
write_u32_le(&mut cu_data, 16, ue_offset_new as u32)?;
write_back_streams(comp, &cu_data, &ppt_data, &other_streams)
}
pub fn encrypt(input_path: &Path, password: &str) -> Result<Vec<u8>> {
let file_data = std::fs::read(input_path)?;
let cursor = Cursor::new(file_data);
let mut comp = CompoundFile::open(cursor)
.map_err(|e| Error::ppt97(format!("encrypt: open OLE2 failed: {}", e)))?;
let streams = read_all_streams(&mut comp)?;
let (cu_data, ppt_data, other_streams) = classify_streams(streams);
let mut ppt_data =
ppt_data.ok_or_else(|| Error::ppt97("encrypt: PowerPoint Document stream not found"))?;
let mut cu_data =
cu_data.ok_or_else(|| Error::ppt97("encrypt: Current User stream not found"))?;
encrypt_ppt_stream(&mut ppt_data, &mut cu_data, password)?;
write_back_streams(comp, &cu_data, &ppt_data, &other_streams)
}
pub fn add_watermark_and_encrypt(
input_path: &Path,
config: &WatermarkConfig,
password: &str,
) -> Result<Vec<u8>> {
let file_data = std::fs::read(input_path)?;
let cursor = Cursor::new(file_data);
let mut comp = CompoundFile::open(cursor).map_err(|e| {
Error::ppt97(format!(
"add_watermark_and_encrypt: open OLE2 failed: {}",
e
))
})?;
let streams = read_all_streams(&mut comp)?;
let (cu_data, ppt_data, other_streams) = classify_streams(streams);
let mut ppt_data = ppt_data.ok_or_else(|| {
Error::ppt97("add_watermark_and_encrypt: PowerPoint Document stream not found")
})?;
let mut cu_data = cu_data
.ok_or_else(|| Error::ppt97("add_watermark_and_encrypt: Current User stream not found"))?;
let (_, _, cu_type, _) = parse_record_header(&cu_data, 0)?;
if cu_type != RT_CURRENT_USER_ATOM {
return Err(Error::ppt97(format!(
"add_watermark_and_encrypt: expected CurrentUserAtom (0x{:04X}), got 0x{:04X}",
RT_CURRENT_USER_ATOM, cu_type
)));
}
let ue_offset_old = read_u32_le(&cu_data, 16)? as usize;
let (_, _, ue_type, ue_len) = parse_record_header(&ppt_data, ue_offset_old)?;
if ue_type != RT_USER_EDIT_ATOM {
return Err(Error::ppt97(format!(
"add_watermark_and_encrypt: expected UserEditAtom (0x{:04X}), got 0x{:04X}",
RT_USER_EDIT_ATOM, ue_type
)));
}
if ue_len != 28 {
return Err(Error::ppt97(format!(
"add_watermark_and_encrypt: file already encrypted or malformed (UserEditAtom.recLen={}, expected 28)",
ue_len
)));
}
let pd_offset_old = read_u32_le(&ppt_data, ue_offset_old + 20)? as usize;
let persist_entries = parse_persist_directory(&ppt_data, pd_offset_old)?;
let (total_inserted, _new_entries, pd_offset_new) =
inject_watermark(&mut ppt_data, config, &persist_entries, pd_offset_old)?;
let ue_offset_new = ue_offset_old + total_inserted;
write_u32_le(&mut ppt_data, ue_offset_new + 20, pd_offset_new as u32)?;
write_u32_le(&mut cu_data, 16, ue_offset_new as u32)?;
encrypt_ppt_stream(&mut ppt_data, &mut cu_data, password)?;
write_back_streams(comp, &cu_data, &ppt_data, &other_streams)
}