Handle encrypted PDFs with empty user password retry
Map lopdf's Unimplemented("encrypted...") error to PdfError::Encrypted
instead of falling through to PdfError::Parse. Retry all Document::load
calls with an empty password when encryption is detected, so
owner-password-only PDFs can be opened. If the retry also fails, the
user now sees a clear "PDF is encrypted" message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1bacf34bd0
commit
433132d195
+28
-4
@@ -98,11 +98,23 @@ pub fn detect_pdf_type_with_config<P: AsRef<Path>>(
|
||||
crate::validate_pdf_file(&path)?;
|
||||
|
||||
// First, load metadata only (fast operation)
|
||||
let metadata = Document::load_metadata(&path)?;
|
||||
let metadata = match Document::load_metadata(&path) {
|
||||
Ok(m) => m,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_metadata_with_password(&path, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
// Then load the full document for content inspection
|
||||
// We use filtered loading to skip heavy objects we don't need
|
||||
let doc = Document::load(&path)?;
|
||||
let doc = match Document::load(&path) {
|
||||
Ok(d) => d,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_with_password(&path, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
detect_from_document(&doc, metadata.page_count, &config)
|
||||
}
|
||||
@@ -120,10 +132,22 @@ pub fn detect_pdf_type_mem_with_config(
|
||||
crate::validate_pdf_bytes(buffer)?;
|
||||
|
||||
// Load metadata first (fast)
|
||||
let metadata = Document::load_metadata_mem(buffer)?;
|
||||
let metadata = match Document::load_metadata_mem(buffer) {
|
||||
Ok(m) => m,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_metadata_mem_with_password(buffer, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
// Load document for inspection
|
||||
let doc = Document::load_mem(buffer)?;
|
||||
let doc = match Document::load_mem(buffer) {
|
||||
Ok(d) => d,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_mem_with_password(buffer, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
detect_from_document(&doc, metadata.page_count, &config)
|
||||
}
|
||||
|
||||
+28
-4
@@ -33,14 +33,26 @@ pub use layout::group_into_lines;
|
||||
/// Extract text from PDF file as plain string
|
||||
pub fn extract_text<P: AsRef<Path>>(path: P) -> Result<String, PdfError> {
|
||||
crate::validate_pdf_file(&path)?;
|
||||
let doc = Document::load(path)?;
|
||||
let doc = match Document::load(&path) {
|
||||
Ok(d) => d,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_with_password(&path, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
extract_text_from_doc(&doc)
|
||||
}
|
||||
|
||||
/// Extract text from PDF memory buffer
|
||||
pub fn extract_text_mem(buffer: &[u8]) -> Result<String, PdfError> {
|
||||
crate::validate_pdf_bytes(buffer)?;
|
||||
let doc = Document::load_mem(buffer)?;
|
||||
let doc = match Document::load_mem(buffer) {
|
||||
Ok(d) => d,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_mem_with_password(buffer, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
extract_text_from_doc(&doc)
|
||||
}
|
||||
|
||||
@@ -76,7 +88,13 @@ pub(crate) fn extract_text_with_positions_and_rects<P: AsRef<Path>>(
|
||||
page_filter: Option<&HashSet<u32>>,
|
||||
) -> Result<(Vec<TextItem>, Vec<PdfRect>), PdfError> {
|
||||
crate::validate_pdf_file(&path)?;
|
||||
let doc = Document::load(path)?;
|
||||
let doc = match Document::load(&path) {
|
||||
Ok(d) => d,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_with_password(&path, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let font_cmaps = FontCMaps::from_doc(&doc);
|
||||
extract_positioned_text_from_doc(&doc, &font_cmaps, page_filter)
|
||||
}
|
||||
@@ -101,7 +119,13 @@ pub(crate) fn extract_text_with_positions_mem_and_rects(
|
||||
page_filter: Option<&HashSet<u32>>,
|
||||
) -> Result<(Vec<TextItem>, Vec<PdfRect>), PdfError> {
|
||||
crate::validate_pdf_bytes(buffer)?;
|
||||
let doc = Document::load_mem(buffer)?;
|
||||
let doc = match Document::load_mem(buffer) {
|
||||
Ok(d) => d,
|
||||
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
|
||||
Document::load_mem_with_password(buffer, "")?
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let font_cmaps = FontCMaps::from_doc(&doc);
|
||||
extract_positioned_text_from_doc(&doc, &font_cmaps, page_filter)
|
||||
}
|
||||
|
||||
+12
@@ -500,6 +500,7 @@ impl From<lopdf::Error> for PdfError {
|
||||
| lopdf::Error::InvalidPassword
|
||||
| lopdf::Error::AlreadyEncrypted
|
||||
| lopdf::Error::UnsupportedSecurityHandler(_) => PdfError::Encrypted,
|
||||
lopdf::Error::Unimplemented(msg) if msg.contains("encrypted") => PdfError::Encrypted,
|
||||
lopdf::Error::Parse(ref pe) if pe.to_string().contains("invalid file header") => {
|
||||
PdfError::NotAPdf("invalid PDF file header".to_string())
|
||||
}
|
||||
@@ -514,6 +515,17 @@ impl From<lopdf::Error> for PdfError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a `lopdf::Error` represents an encryption-related failure.
|
||||
pub(crate) fn is_encrypted_lopdf_error(e: &lopdf::Error) -> bool {
|
||||
matches!(
|
||||
e,
|
||||
lopdf::Error::Decryption(_)
|
||||
| lopdf::Error::InvalidPassword
|
||||
| lopdf::Error::AlreadyEncrypted
|
||||
| lopdf::Error::UnsupportedSecurityHandler(_)
|
||||
) || matches!(e, lopdf::Error::Unimplemented(msg) if msg.contains("encrypted"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PDF validation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user