use crate::error::{Error, Result};
use crate::model::Metadata;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct Relationship {
pub id: String,
pub rel_type: String,
pub target: String,
pub external: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Relationships {
pub by_id: HashMap<String, Relationship>,
pub by_type: HashMap<String, Vec<Relationship>>,
}
impl Relationships {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, id: &str) -> Option<&Relationship> {
self.by_id.get(id)
}
pub fn get_by_type(&self, rel_type: &str) -> Vec<&Relationship> {
self.by_type
.get(rel_type)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
pub fn add(&mut self, rel: Relationship) {
self.by_type
.entry(rel.rel_type.clone())
.or_default()
.push(rel.clone());
self.by_id.insert(rel.id.clone(), rel);
}
}
fn fix_xml_encoding_declaration(content: &str) -> String {
if content.starts_with("<?xml") {
if let Some(end_decl) = content.find("?>") {
let decl = &content[..end_decl + 2];
let rest = &content[end_decl + 2..];
let fixed_decl = decl
.replace("encoding=\"UTF-16\"", "encoding=\"UTF-8\"")
.replace("encoding='UTF-16'", "encoding='UTF-8'")
.replace("encoding=\"utf-16\"", "encoding=\"UTF-8\"")
.replace("encoding='utf-16'", "encoding='UTF-8'");
return format!("{}{}", fixed_decl, rest);
}
}
content.to_string()
}
pub struct OoxmlContainer {
archive: RefCell<zip::ZipArchive<Cursor<Vec<u8>>>>,
#[allow(dead_code)]
package_rels: Option<Relationships>,
}
pub fn decode_xml_bytes(bytes: &[u8]) -> Result<String> {
if bytes.len() >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF {
return String::from_utf8(bytes[3..].to_vec())
.map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)));
}
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
let content = decode_utf16_le(&bytes[2..])?;
return Ok(fix_xml_encoding_declaration(&content));
}
if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
let content = decode_utf16_be(&bytes[2..])?;
return Ok(fix_xml_encoding_declaration(&content));
}
match String::from_utf8(bytes.to_vec()) {
Ok(s) => Ok(s),
Err(_) => {
if bytes.len() >= 4 && bytes[1] == 0 && bytes[3] == 0 {
decode_utf16_le(bytes)
} else if bytes.len() >= 4 && bytes[0] == 0 && bytes[2] == 0 {
decode_utf16_be(bytes)
} else {
Ok(String::from_utf8_lossy(bytes).into_owned())
}
}
}
}
fn decode_utf16_le(bytes: &[u8]) -> Result<String> {
let len = bytes.len() & !1;
let u16_iter = (0..len)
.step_by(2)
.map(|i| u16::from_le_bytes([bytes[i], bytes[i + 1]]));
char::decode_utf16(u16_iter)
.collect::<std::result::Result<String, _>>()
.map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
}
fn decode_utf16_be(bytes: &[u8]) -> Result<String> {
let len = bytes.len() & !1;
let u16_iter = (0..len)
.step_by(2)
.map(|i| u16::from_be_bytes([bytes[i], bytes[i + 1]]));
char::decode_utf16(u16_iter)
.collect::<std::result::Result<String, _>>()
.map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
}
impl OoxmlContainer {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let file = File::open(path.as_ref())?;
let mut reader = BufReader::new(file);
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
Self::from_bytes(data)
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
let cursor = Cursor::new(data);
let archive = zip::ZipArchive::new(cursor)?;
Ok(Self {
archive: RefCell::new(archive),
package_rels: None,
})
}
pub fn from_reader<R: Read + Seek>(mut reader: R) -> Result<Self> {
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
Self::from_bytes(data)
}
pub fn read_xml(&self, path: &str) -> Result<String> {
let mut archive = self.archive.borrow_mut();
let mut file = archive
.by_name(path)
.map_err(|_| Error::MissingComponent(path.to_string()))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let content = decode_xml_bytes(&bytes)?;
Ok(content)
}
pub fn read_binary(&self, path: &str) -> Result<Vec<u8>> {
let mut archive = self.archive.borrow_mut();
let mut file = archive
.by_name(path)
.map_err(|_| Error::MissingComponent(path.to_string()))?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
Ok(data)
}
pub fn exists(&self, path: &str) -> bool {
let archive = self.archive.borrow();
let result = archive.file_names().any(|n| n == path);
result
}
pub fn list_files(&self) -> Vec<String> {
let archive = self.archive.borrow();
archive.file_names().map(String::from).collect()
}
pub fn list_files_with_prefix(&self, prefix: &str) -> Vec<String> {
let archive = self.archive.borrow();
archive
.file_names()
.filter(|n| n.starts_with(prefix))
.map(String::from)
.collect()
}
pub fn read_relationships(&self, part_path: &str) -> Result<Relationships> {
let rels_path = if part_path.is_empty() || part_path == "/" {
"_rels/.rels".to_string()
} else {
let path = Path::new(part_path);
let parent = path.parent().unwrap_or(Path::new(""));
let filename = path.file_name().unwrap_or_default().to_string_lossy();
format!("{}/_rels/{}.rels", parent.display(), filename)
};
self.parse_relationships(&rels_path)
}
pub fn read_package_relationships(&self) -> Result<Relationships> {
self.parse_relationships("_rels/.rels")
}
pub fn parse_core_metadata(&self) -> Result<Metadata> {
let mut meta = Metadata::default();
if let Ok(xml) = self.read_xml("docProps/core.xml") {
let mut reader = quick_xml::Reader::from_str(&xml);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut current_element: Option<String> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(quick_xml::events::Event::Start(e)) => {
let name = e.name();
current_element =
Some(String::from_utf8_lossy(name.local_name().as_ref()).to_string());
}
Ok(quick_xml::events::Event::Text(e)) => {
if let Some(ref elem) = current_element {
let text = e.unescape().unwrap_or_default().to_string();
match elem.as_str() {
"title" => meta.title = Some(text),
"creator" => meta.author = Some(text),
"subject" => meta.subject = Some(text),
"description" => meta.description = Some(text),
"keywords" => {
meta.keywords = text
.split([',', ';'])
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
"created" => meta.created = Some(text),
"modified" => meta.modified = Some(text),
"lastModifiedBy" => meta.last_modified_by = Some(text),
_ => {}
}
}
}
Ok(quick_xml::events::Event::End(_)) => {
current_element = None;
}
Ok(quick_xml::events::Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
}
self.parse_app_metadata(&mut meta);
Ok(meta)
}
fn parse_app_metadata(&self, meta: &mut Metadata) {
let xml = match self.read_xml("docProps/app.xml") {
Ok(xml) => xml,
Err(_) => return,
};
let mut reader = quick_xml::Reader::from_str(&xml);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut current_element: Option<String> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(quick_xml::events::Event::Start(e)) => {
let name = e.name();
current_element =
Some(String::from_utf8_lossy(name.local_name().as_ref()).to_string());
}
Ok(quick_xml::events::Event::Text(e)) => {
if let Some(ref elem) = current_element {
let text = e.unescape().unwrap_or_default().to_string();
match elem.as_str() {
"Application" => meta.application = Some(text),
"Pages" => {
if meta.page_count.is_none() {
meta.page_count = text.parse::<u32>().ok();
}
}
"Words" => {
if meta.word_count.is_none() {
meta.word_count = text.parse::<u32>().ok();
}
}
"Slides" => {
if meta.page_count.is_none() {
meta.page_count = text.parse::<u32>().ok();
}
}
_ => {}
}
}
}
Ok(quick_xml::events::Event::End(_)) => {
current_element = None;
}
Ok(quick_xml::events::Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
}
fn parse_relationships(&self, rels_path: &str) -> Result<Relationships> {
let content = match self.read_xml(rels_path) {
Ok(c) => c,
Err(_) => return Ok(Relationships::new()),
};
if content.trim().is_empty() {
return Ok(Relationships::new());
}
let mut rels = Relationships::new();
let mut reader = quick_xml::Reader::from_str(&content);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(quick_xml::events::Event::Empty(e)) if e.name().as_ref() == b"Relationship" => {
let mut id = String::new();
let mut rel_type = String::new();
let mut target = String::new();
let mut external = false;
for attr in e.attributes().flatten() {
match attr.key.as_ref() {
b"Id" => id = String::from_utf8_lossy(&attr.value).to_string(),
b"Type" => rel_type = String::from_utf8_lossy(&attr.value).to_string(),
b"Target" => target = String::from_utf8_lossy(&attr.value).to_string(),
b"TargetMode" => {
external = String::from_utf8_lossy(&attr.value).to_lowercase()
== "external"
}
_ => {}
}
}
if !id.is_empty() {
rels.add(Relationship {
id,
rel_type,
target,
external,
});
}
}
Ok(quick_xml::events::Event::Eof) => break,
Err(e) => return Err(Error::XmlParse(e.to_string())),
_ => {}
}
buf.clear();
}
Ok(rels)
}
pub fn resolve_path(base: &str, relative: &str) -> String {
if let Some(stripped) = relative.strip_prefix('/') {
return stripped.to_string();
}
let base_path = Path::new(base);
let base_dir = base_path.parent().unwrap_or(Path::new(""));
let mut result = base_dir.to_path_buf();
for component in Path::new(relative).components() {
match component {
std::path::Component::ParentDir => {
result.pop();
}
std::path::Component::Normal(c) => {
result.push(c);
}
_ => {}
}
}
result.to_string_lossy().replace('\\', "/")
}
}
impl std::fmt::Debug for OoxmlContainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OoxmlContainer")
.field("files", &self.list_files().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_path() {
assert_eq!(
OoxmlContainer::resolve_path("word/document.xml", "../media/image1.png"),
"media/image1.png"
);
assert_eq!(
OoxmlContainer::resolve_path("word/document.xml", "styles.xml"),
"word/styles.xml"
);
assert_eq!(
OoxmlContainer::resolve_path("xl/worksheets/sheet1.xml", "../sharedStrings.xml"),
"xl/sharedStrings.xml"
);
assert_eq!(
OoxmlContainer::resolve_path("ppt/slides/slide1.xml", "/ppt/media/image1.png"),
"ppt/media/image1.png"
);
}
#[test]
fn test_relationships_collection() {
let mut rels = Relationships::new();
rels.add(Relationship {
id: "rId1".to_string(),
rel_type: "http://test/type1".to_string(),
target: "target1.xml".to_string(),
external: false,
});
rels.add(Relationship {
id: "rId2".to_string(),
rel_type: "http://test/type1".to_string(),
target: "target2.xml".to_string(),
external: false,
});
assert!(rels.get("rId1").is_some());
assert!(rels.get("rId3").is_none());
assert_eq!(rels.get_by_type("http://test/type1").len(), 2);
}
#[test]
fn test_open_docx() {
let path = "test-files/file-sample_1MB.docx";
if std::path::Path::new(path).exists() {
let container = OoxmlContainer::open(path).unwrap();
assert!(container.exists("[Content_Types].xml"));
assert!(container.exists("word/document.xml"));
let files = container.list_files();
assert!(!files.is_empty());
let rels = container.read_package_relationships().unwrap();
assert!(!rels.by_id.is_empty());
}
}
#[test]
fn test_open_xlsx() {
let path = "test-files/file_example_XLSX_5000.xlsx";
if std::path::Path::new(path).exists() {
let container = OoxmlContainer::open(path).unwrap();
assert!(container.exists("[Content_Types].xml"));
assert!(container.exists("xl/workbook.xml"));
let xl_files = container.list_files_with_prefix("xl/");
assert!(!xl_files.is_empty());
}
}
#[test]
fn test_utf16_xml_reading() {
let path = "test-files/officedissector/test/unit_test/testdocs/testutf16.docx";
if std::path::Path::new(path).exists() {
let container = OoxmlContainer::open(path).unwrap();
let content = container
.read_xml("[Content_Types].xml")
.expect("Should read UTF-16 XML");
assert!(
content.contains("ContentType"),
"Content should contain ContentType"
);
assert!(
!content.starts_with("\0"),
"Should not start with null byte"
);
assert!(
content.starts_with("<?xml"),
"Should start with XML declaration"
);
let doc_xml = container
.read_xml("word/document.xml")
.expect("Should read UTF-16 document.xml");
assert!(
doc_xml.contains("w:document"),
"Should contain w:document element"
);
assert!(
doc_xml.contains("Footnote in section"),
"Should contain document text"
);
}
}
#[test]
fn test_utf16_decoding_function() {
let utf16_le = b"\xFF\xFE<\0?\0x\0m\0l\0>\0";
let result = decode_xml_bytes(utf16_le).expect("Should decode UTF-16 LE");
assert_eq!(result, "<?xml>");
let utf16_be = b"\xFE\xFF\0<\0?\0x\0m\0l\0>";
let result = decode_xml_bytes(utf16_be).expect("Should decode UTF-16 BE");
assert_eq!(result, "<?xml>");
let utf8_bom = b"\xEF\xBB\xBF<?xml>";
let result = decode_xml_bytes(utf8_bom).expect("Should decode UTF-8 with BOM");
assert_eq!(result, "<?xml>");
let utf8_plain = b"<?xml>";
let result = decode_xml_bytes(utf8_plain).expect("Should decode UTF-8 without BOM");
assert_eq!(result, "<?xml>");
}
#[test]
fn test_utf16_full_parse() {
let path = "test-files/officedissector/test/unit_test/testdocs/testutf16.docx";
if std::path::Path::new(path).exists() {
let container = OoxmlContainer::open(path).unwrap();
for file_path in [
"word/styles.xml",
"word/numbering.xml",
"word/document.xml",
"docProps/core.xml",
"word/footnotes.xml",
"word/endnotes.xml",
] {
match container.read_xml(file_path) {
Ok(content) => {
println!(
"{}: {} bytes, empty={}",
file_path,
content.len(),
content.trim().is_empty()
);
if content.len() > 0 {
let preview = &content[..content.len().min(100)];
println!(" Preview: {}", preview.replace('\n', "\\n"));
}
}
Err(e) => {
println!("{}: ERROR - {:?}", file_path, e);
}
}
}
println!("\n=== Testing raw styles.xml ===");
match container.read_binary("word/styles.xml") {
Ok(data) => {
println!("Raw bytes: {} bytes", data.len());
println!("First 10 bytes: {:02x?}", &data[..10.min(data.len())]);
println!(
"Last 10 bytes: {:02x?}",
&data[data.len().saturating_sub(10)..]
);
let decoded = decode_xml_bytes(&data).expect("decode failed");
println!("Decoded: {} chars", decoded.len());
println!(
"Decoded first 100: {:?}",
&decoded[..100.min(decoded.len())]
);
println!(
"Decoded last 100: {:?}",
&decoded[decoded.len().saturating_sub(100)..]
);
let null_count = decoded.bytes().filter(|&b| b == 0).count();
println!("Null bytes after decode: {}", null_count);
}
Err(e) => println!("read_binary ERROR: {:?}", e),
}
println!("\n=== Testing StyleMap ===");
match container.read_xml("word/styles.xml") {
Ok(xml) => {
println!("Read styles.xml: {} bytes", xml.len());
let first_100 = &xml[..xml.len().min(100)];
let last_100 = if xml.len() > 100 {
&xml[xml.len() - 100..]
} else {
&xml
};
println!("First 100: {:?}", first_100);
println!("Last 100: {:?}", last_100);
let null_count = xml.bytes().filter(|&b| b == 0).count();
println!("Null bytes in string: {}", null_count);
match crate::docx::styles::StyleMap::parse(&xml) {
Ok(styles) => println!("Styles OK: {} styles", styles.styles.len()),
Err(e) => println!("Styles ERROR: {:?}", e),
}
}
Err(e) => {
println!("read_xml ERROR: {:?}", e);
}
}
println!("\n=== Testing DocxParser ===");
match crate::docx::DocxParser::open(path) {
Ok(mut parser) => {
println!("DocxParser init OK");
match parser.parse() {
Ok(doc) => {
println!("Parse OK: {} sections", doc.sections.len());
println!(
"Text: {}",
&doc.plain_text()[..doc.plain_text().len().min(200)]
);
}
Err(e) => {
println!("Parse ERROR: {:?}", e);
}
}
}
Err(e) => {
println!("DocxParser init ERROR: {:?}", e);
}
}
}
}
#[test]
fn test_open_pptx() {
let path = "test-files/file_example_PPT_1MB.pptx";
if std::path::Path::new(path).exists() {
let container = OoxmlContainer::open(path).unwrap();
assert!(container.exists("[Content_Types].xml"));
assert!(container.exists("ppt/presentation.xml"));
let slides = container.list_files_with_prefix("ppt/slides/");
assert!(!slides.is_empty());
}
}
fn create_test_zip(entries: &[(&str, &str)]) -> Vec<u8> {
use std::io::Write;
let buf = Vec::new();
let cursor = Cursor::new(buf);
let mut zip = zip::ZipWriter::new(cursor);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
for (name, content) in entries {
zip.start_file(*name, options).unwrap();
zip.write_all(content.as_bytes()).unwrap();
}
let cursor = zip.finish().unwrap();
cursor.into_inner()
}
#[test]
fn test_parse_core_metadata_last_modified_by() {
let core_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/">
<dc:title>Test Title</dc:title>
<dc:creator>Author Name</dc:creator>
<cp:lastModifiedBy>Editor Name</cp:lastModifiedBy>
<dcterms:created>2024-01-01T00:00:00Z</dcterms:created>
</cp:coreProperties>"#;
let data = create_test_zip(&[
("[Content_Types].xml", "<Types/>"),
("docProps/core.xml", core_xml),
]);
let container = OoxmlContainer::from_bytes(data).unwrap();
let meta = container.parse_core_metadata().unwrap();
assert_eq!(meta.title.as_deref(), Some("Test Title"));
assert_eq!(meta.author.as_deref(), Some("Author Name"));
assert_eq!(meta.last_modified_by.as_deref(), Some("Editor Name"));
assert_eq!(meta.created.as_deref(), Some("2024-01-01T00:00:00Z"));
}
#[test]
fn test_parse_app_metadata_basic() {
let app_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Application>Microsoft Office Word</Application>
<Pages>5</Pages>
<Words>1234</Words>
</Properties>"#;
let data = create_test_zip(&[
("[Content_Types].xml", "<Types/>"),
("docProps/app.xml", app_xml),
]);
let container = OoxmlContainer::from_bytes(data).unwrap();
let meta = container.parse_core_metadata().unwrap();
assert_eq!(meta.application.as_deref(), Some("Microsoft Office Word"));
assert_eq!(meta.page_count, Some(5));
assert_eq!(meta.word_count, Some(1234));
}
#[test]
fn test_parse_app_metadata_slides() {
let app_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Application>Microsoft Office PowerPoint</Application>
<Slides>10</Slides>
</Properties>"#;
let data = create_test_zip(&[
("[Content_Types].xml", "<Types/>"),
("docProps/app.xml", app_xml),
]);
let container = OoxmlContainer::from_bytes(data).unwrap();
let meta = container.parse_core_metadata().unwrap();
assert_eq!(
meta.application.as_deref(),
Some("Microsoft Office PowerPoint")
);
assert_eq!(meta.page_count, Some(10));
assert_eq!(meta.word_count, None);
}
#[test]
fn test_parse_app_metadata_pages_does_not_override_slides() {
let app_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Application>Test</Application>
<Pages>3</Pages>
<Slides>10</Slides>
</Properties>"#;
let data = create_test_zip(&[
("[Content_Types].xml", "<Types/>"),
("docProps/app.xml", app_xml),
]);
let container = OoxmlContainer::from_bytes(data).unwrap();
let meta = container.parse_core_metadata().unwrap();
assert_eq!(meta.page_count, Some(3));
}
#[test]
fn test_parse_app_metadata_missing_file() {
let data = create_test_zip(&[("[Content_Types].xml", "<Types/>")]);
let container = OoxmlContainer::from_bytes(data).unwrap();
let meta = container.parse_core_metadata().unwrap();
assert_eq!(meta.application, None);
assert_eq!(meta.page_count, None);
assert_eq!(meta.word_count, None);
}
#[test]
fn test_parse_combined_core_and_app_metadata() {
let core_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>My Document</dc:title>
<dc:creator>Jane Doe</dc:creator>
<cp:lastModifiedBy>John Smith</cp:lastModifiedBy>
</cp:coreProperties>"#;
let app_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Application>LibreOffice</Application>
<Pages>12</Pages>
<Words>5000</Words>
</Properties>"#;
let data = create_test_zip(&[
("[Content_Types].xml", "<Types/>"),
("docProps/core.xml", core_xml),
("docProps/app.xml", app_xml),
]);
let container = OoxmlContainer::from_bytes(data).unwrap();
let meta = container.parse_core_metadata().unwrap();
assert_eq!(meta.title.as_deref(), Some("My Document"));
assert_eq!(meta.author.as_deref(), Some("Jane Doe"));
assert_eq!(meta.last_modified_by.as_deref(), Some("John Smith"));
assert_eq!(meta.application.as_deref(), Some("LibreOffice"));
assert_eq!(meta.page_count, Some(12));
assert_eq!(meta.word_count, Some(5000));
}
#[test]
fn test_docx_metadata_with_app() {
let path = "test-files/file-sample_1MB.docx";
if std::path::Path::new(path).exists() {
let container = OoxmlContainer::open(path).unwrap();
let meta = container.parse_core_metadata().unwrap();
println!("Application: {:?}", meta.application);
println!("Page count: {:?}", meta.page_count);
println!("Word count: {:?}", meta.word_count);
println!("Last modified by: {:?}", meta.last_modified_by);
}
}
}