#[cfg(not(feature = "async"))]
mod sync_tests {
use lopdf::{Document, LoadOptions, Object};
use std::fs::File;
#[test]
fn load_options_default() {
let opts = LoadOptions::default();
assert!(opts.password.is_none());
assert!(opts.filter.is_none());
assert!(!opts.strict);
}
#[test]
fn load_options_with_password() {
let opts = LoadOptions::with_password("secret");
assert_eq!(opts.password.as_deref(), Some("secret"));
assert!(opts.filter.is_none());
assert!(!opts.strict);
}
#[test]
fn load_options_with_filter() {
fn my_filter(id: (u32, u16), obj: &mut Object) -> Option<((u32, u16), Object)> {
Some((id, obj.clone()))
}
let opts = LoadOptions::with_filter(my_filter);
assert!(opts.password.is_none());
assert!(opts.filter.is_some());
assert!(!opts.strict);
}
#[test]
fn load_options_debug_masks_password() {
let opts = LoadOptions::with_password("supersecret");
let debug = format!("{:?}", opts);
assert!(
!debug.contains("supersecret"),
"password should be masked in Debug output"
);
assert!(debug.contains("***"), "masked password should show ***");
}
#[test]
fn load_options_debug_no_password() {
let opts = LoadOptions::default();
let debug = format!("{:?}", opts);
assert!(debug.contains("None"));
}
#[test]
fn load_with_options_default_matches_load() {
let doc1 = Document::load("assets/example.pdf").unwrap();
let doc2 = Document::load_with_options("assets/example.pdf", LoadOptions::default()).unwrap();
assert_eq!(doc1.version, doc2.version);
assert_eq!(doc1.objects.len(), doc2.objects.len());
}
#[test]
fn load_from_with_options_default_matches_load_from() {
let doc1 = Document::load_from(File::open("assets/example.pdf").unwrap()).unwrap();
let doc2 = Document::load_from_with_options(File::open("assets/example.pdf").unwrap(), LoadOptions::default())
.unwrap();
assert_eq!(doc1.version, doc2.version);
assert_eq!(doc1.objects.len(), doc2.objects.len());
}
#[test]
fn load_mem_with_options_default_matches_load_mem() {
let buf = std::fs::read("assets/example.pdf").unwrap();
let doc1 = Document::load_mem(&buf).unwrap();
let doc2 = Document::load_mem_with_options(&buf, LoadOptions::default()).unwrap();
assert_eq!(doc1.version, doc2.version);
assert_eq!(doc1.objects.len(), doc2.objects.len());
}
#[test]
fn load_with_options_filter_removes_objects() {
fn drop_streams(id: (u32, u16), obj: &mut Object) -> Option<((u32, u16), Object)> {
if obj.as_stream().is_ok() {
None
} else {
Some((id, obj.clone()))
}
}
let full = Document::load("assets/example.pdf").unwrap();
let filtered =
Document::load_with_options("assets/example.pdf", LoadOptions::with_filter(drop_streams)).unwrap();
let full_streams = full.objects.values().filter(|o| o.as_stream().is_ok()).count();
let filtered_streams = filtered.objects.values().filter(|o| o.as_stream().is_ok()).count();
assert!(full_streams > 0, "example.pdf should have streams");
assert_eq!(filtered_streams, 0, "filter should have removed all streams");
assert!(
filtered.objects.len() < full.objects.len(),
"filtered doc should have fewer objects"
);
}
#[test]
fn load_mem_with_options_filter() {
fn only_dicts(id: (u32, u16), obj: &mut Object) -> Option<((u32, u16), Object)> {
if obj.as_dict().is_ok() {
Some((id, obj.clone()))
} else {
None
}
}
let buf = std::fs::read("assets/example.pdf").unwrap();
let filtered = Document::load_mem_with_options(&buf, LoadOptions::with_filter(only_dicts)).unwrap();
for obj in filtered.objects.values() {
assert!(obj.as_dict().is_ok(), "all objects should be dictionaries after filter");
}
}
#[test]
fn load_with_options_strict_false_loads_normally() {
let doc = Document::load_with_options(
"assets/example.pdf",
LoadOptions {
strict: false,
..Default::default()
},
)
.unwrap();
assert_eq!(doc.version, "1.5");
}
#[test]
fn load_with_options_strict_true_loads_valid_pdf() {
let doc = Document::load_with_options(
"assets/example.pdf",
LoadOptions {
strict: true,
..Default::default()
},
)
.unwrap();
assert_eq!(doc.version, "1.5");
}
#[test]
fn load_with_options_password_on_encrypted_pdf() {
let doc = Document::load_with_options("assets/encrypted.pdf", LoadOptions::with_password("")).unwrap();
assert!(!doc.is_encrypted());
assert!(doc.encryption_state.is_some());
let pages = doc.get_pages();
assert_eq!(pages.len(), 1);
}
#[test]
fn load_mem_with_options_password_on_encrypted_pdf() {
let buf = std::fs::read("assets/encrypted.pdf").unwrap();
let doc = Document::load_mem_with_options(&buf, LoadOptions::with_password("")).unwrap();
assert!(!doc.is_encrypted());
assert!(doc.encryption_state.is_some());
}
#[test]
fn load_with_options_combined_password_and_strict() {
let opts = LoadOptions {
password: Some(String::new()),
strict: true,
..Default::default()
};
let doc = Document::load_with_options("assets/encrypted.pdf", opts).unwrap();
assert!(!doc.is_encrypted());
assert!(doc.encryption_state.is_some());
}
#[test]
fn load_with_options_nonexistent_file() {
let result = Document::load_with_options("nonexistent.pdf", LoadOptions::default());
assert!(result.is_err());
}
#[test]
fn strict_rejects_binary_bytes_on_header_line() {
let buf = b"%PDF-1.3 \xb0\x9f\x92\x9c\r%%EOF\r";
let result = Document::load_mem_with_options(
buf,
LoadOptions {
strict: true,
..Default::default()
},
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid file header"),
"expected InvalidFileHeader, got: {err}"
);
}
#[test]
fn lenient_accepts_binary_bytes_on_header_line() {
let buf = b"%PDF-1.3 \xb0\x9f\x92\x9c\r%%EOF\r";
let result = Document::load_mem_with_options(buf, LoadOptions::default());
if let Err(e) = &result {
assert!(
!e.to_string().contains("invalid file header"),
"lenient mode should accept binary bytes on header line, got: {e}"
);
}
}
}