use std::path::Path;
use std::io::Read;
#[cfg(feature = "iwa")]
use std::io::Cursor;
use crate::common::detection::FileFormat;
pub fn detect_iwork_format(bytes: &[u8]) -> Option<FileFormat> {
if bytes.len() < 4 || &bytes[0..4] != crate::common::detection::utils::ZIP_SIGNATURE {
return None;
}
let cursor = std::io::Cursor::new(bytes);
match zip::ZipArchive::new(cursor) {
Ok(mut archive) => {
let has_iwa_files = (0..archive.len()).any(|i| {
archive.by_index(i).ok()
.map(|file| file.name().ends_with(".iwa"))
.unwrap_or(false)
});
if !has_iwa_files {
return None;
}
detect_application_from_zip_archive(&mut archive)
}
Err(_) => None,
}
}
pub fn detect_iwork_format_from_reader<R: std::io::Read + std::io::Seek>(
reader: &mut R
) -> Option<FileFormat> {
let mut header = [0u8; 4];
if reader.read_exact(&mut header).is_err() {
return None;
}
let _ = reader.seek(std::io::SeekFrom::Start(0));
if &header[0..4] == crate::common::detection::utils::ZIP_SIGNATURE {
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
let has_iwa_files = (0..archive.len()).any(|i| {
archive.by_index(i).ok()
.map(|file| file.name().ends_with(".iwa"))
.unwrap_or(false)
});
if has_iwa_files {
return detect_application_from_zip_archive(&mut archive);
}
}
}
None
}
pub fn detect_iwork_format_from_path<P: AsRef<Path>>(path: P) -> Option<FileFormat> {
let path = path.as_ref();
if !path.is_dir() {
if let Ok(data) = std::fs::read(path) {
return detect_iwork_format(&data);
}
return None;
}
if let Some(format) = detect_iwork_format_from_files(path) {
return Some(format);
}
#[cfg(feature = "iwa")]
{
match crate::iwa::bundle::Bundle::open(path) {
Ok(bundle) => {
let all_message_types: Vec<u32> = bundle.archives()
.values()
.flat_map(|archive| &archive.objects)
.flat_map(|obj| &obj.messages)
.map(|msg| msg.type_)
.collect();
match crate::iwa::registry::detect_application(&all_message_types) {
Some(app) => {
let format = match app {
crate::iwa::registry::Application::Pages => FileFormat::Pages,
crate::iwa::registry::Application::Keynote => FileFormat::Keynote,
crate::iwa::registry::Application::Numbers => FileFormat::Numbers,
crate::iwa::registry::Application::Common => return None, };
Some(format)
}
None => None,
}
}
Err(_) => None,
}
}
#[cfg(not(feature = "iwa"))]
None
}
fn detect_iwork_format_from_files(bundle_path: &Path) -> Option<FileFormat> {
let keynote_index = bundle_path.join("index.apxl");
if keynote_index.exists() && keynote_index.is_file() {
return Some(FileFormat::Keynote);
}
let pages_index = bundle_path.join("index.xml");
if pages_index.exists() && pages_index.is_file() {
return Some(FileFormat::Pages);
}
let numbers_index = bundle_path.join("index.numbers");
if numbers_index.exists() && numbers_index.is_file() {
return Some(FileFormat::Numbers);
}
let keynote_data = bundle_path.join("Data");
if keynote_data.exists() && keynote_data.is_dir() {
let keynote_theme = bundle_path.join("theme-preview.jpg");
let keynote_assets = bundle_path.join("Assets");
if keynote_theme.exists() || (keynote_assets.exists() && keynote_assets.is_dir()) {
return Some(FileFormat::Keynote);
}
}
let numbers_calc = bundle_path.join("Index");
if numbers_calc.exists() && numbers_calc.is_dir() {
let calc_files = std::fs::read_dir(&numbers_calc).ok()?;
for entry in calc_files.flatten() {
if entry.path().extension().is_some_and(|ext| ext == "iwa") {
return Some(FileFormat::Numbers);
}
}
}
None
}
pub fn detect_application_from_zip_archive<R: Read + std::io::Seek>(archive: &mut zip::ZipArchive<R>) -> Option<FileFormat> {
let mut table_file_count = 0;
let mut has_calculation_engine = false;
let mut slide_file_count = 0;
let mut file_names = Vec::new();
for i in 0..archive.len() {
if let Ok(zip_file) = archive.by_index(i) {
let name = zip_file.name();
file_names.push(name.to_string());
if name.starts_with("Index/Tables/") && name.ends_with(".iwa") {
table_file_count += 1;
}
if name == "Index/CalculationEngine.iwa" {
has_calculation_engine = true;
}
if (name.starts_with("Index/Slide") || name.starts_with("Index/TemplateSlide")) && name.ends_with(".iwa") {
slide_file_count += 1;
}
}
}
if slide_file_count > 0 && table_file_count == 0 {
Some(FileFormat::Keynote)
} else if slide_file_count > 0 && has_calculation_engine {
Some(FileFormat::Keynote)
} else if table_file_count > 0 && slide_file_count == 0 {
if has_calculation_engine {
Some(FileFormat::Numbers)
} else {
Some(FileFormat::Pages)
}
} else if slide_file_count > 0 {
Some(FileFormat::Keynote)
} else if has_calculation_engine {
Some(FileFormat::Numbers)
} else {
detect_application_from_message_types(archive)
}
}
#[cfg(feature = "iwa")]
fn detect_application_from_message_types<R: Read + std::io::Seek>(archive: &mut zip::ZipArchive<R>) -> Option<FileFormat> {
let mut all_message_types = Vec::new();
for i in 0..archive.len() {
if let Ok(mut zip_file) = archive.by_index(i) && zip_file.name().ends_with(".iwa") {
let mut compressed_data = Vec::new();
if zip_file.read_to_end(&mut compressed_data).is_err() {
continue; }
let Ok(decompressed) = crate::iwa::snappy::SnappyStream::decompress(&mut Cursor::new(&compressed_data)) else {
continue; };
let Ok(iwa_archive) = crate::iwa::archive::Archive::parse(decompressed.data()) else {
continue; };
for object in &iwa_archive.objects {
for message in &object.messages {
all_message_types.push(message.type_);
}
}
}
}
let mut pages_score = 0;
let mut keynote_score = 0;
let mut numbers_score = 0;
for &type_id in &all_message_types {
match type_id {
10000..=19999 => pages_score += 1, 200..=999 => keynote_score += 1, 1..=199 => numbers_score += 1, _ => {}
}
}
if pages_score > keynote_score && pages_score > numbers_score {
Some(FileFormat::Pages)
} else if keynote_score > pages_score && keynote_score > numbers_score {
Some(FileFormat::Keynote)
} else if numbers_score > pages_score && numbers_score > keynote_score {
Some(FileFormat::Numbers)
} else {
None
}
}
#[cfg(not(feature = "iwa"))]
fn detect_application_from_message_types<R: Read + std::io::Seek>(_archive: &mut zip::ZipArchive<R>) -> Option<FileFormat> {
None
}