Skip to main content

docling_core/
encryption.rs

1//! Why an encrypted document could not be opened — the one typed signal every
2//! backend raises for a password-protected input (#636).
3//!
4//! A PDF, an Office document, an iWork package and a WordPerfect file each
5//! detect encryption in their own way and word the failure for their own
6//! users, but a caller has one question: *is this a password problem, and
7//! would asking for one help?* Before this type, the answer meant matching
8//! on message text ("document is encrypted", "the PDF is encrypted …",
9//! "password-protected"), which no release promised to keep. The value lives
10//! here, in the crate every backend already depends on, so `docling-pdf`'s
11//! error and the converter's error can both carry it; the converter's
12//! `ConversionError::encryption()` finds it anywhere on the `source()` chain.
13//!
14//! The [`Display`](std::fmt::Display) text is the Office backends' wording
15//! (`document is encrypted (a password is required to open it)`, …), which a
16//! backend prefixes with its format; backends with an established wording of
17//! their own (the PDF reader, iWork, WordPerfect) keep it and attach the
18//! typed value as the cause, so no error message changed with its arrival.
19
20use std::fmt;
21
22/// Why an encrypted document could not be opened.
23///
24/// Only [`NeedPassword`](Self::NeedPassword) and
25/// [`WrongPassword`](Self::WrongPassword) are solved by a password
26/// ([`needs_password`](Self::needs_password)); the other two say the file
27/// cannot be read however it is asked. Non-exhaustive: a reader may learn to
28/// tell another case apart.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum EncryptionError {
32    /// No password was given and none the reader tries by itself (a
33    /// format's documented default) opens the document.
34    NeedPassword,
35    /// A password was given and neither it nor a default one opens it.
36    WrongPassword,
37    /// The document is encrypted in a way this reader does not decrypt
38    /// (ODF package encryption, XOR obfuscation, an iWork package, a
39    /// WordPerfect file, an unknown cipher), so no password would help. The
40    /// payload names the scheme.
41    NotDecryptable(String),
42    /// The document is encrypted, but its encryption header is damaged or
43    /// outside the specification's bounds. The payload names the field.
44    Malformed(String),
45}
46
47impl EncryptionError {
48    /// Whether a (different) password would open the document — the two
49    /// cases worth prompting a user for.
50    pub fn needs_password(&self) -> bool {
51        matches!(self, Self::NeedPassword | Self::WrongPassword)
52    }
53
54    /// A stable machine-readable name for the case — what an HTTP error
55    /// body or a log line carries: `password_required`, `wrong_password`,
56    /// `not_decryptable`, `malformed_encryption`.
57    pub fn code(&self) -> &'static str {
58        match self {
59            Self::NeedPassword => "password_required",
60            Self::WrongPassword => "wrong_password",
61            Self::NotDecryptable(_) => "not_decryptable",
62            Self::Malformed(_) => "malformed_encryption",
63        }
64    }
65}
66
67impl fmt::Display for EncryptionError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Self::NeedPassword => {
71                f.write_str("document is encrypted (a password is required to open it)")
72            }
73            Self::WrongPassword => f.write_str("document is encrypted and the password is wrong"),
74            Self::NotDecryptable(what) => {
75                write!(
76                    f,
77                    "document is encrypted with an unsupported scheme ({what})"
78                )
79            }
80            Self::Malformed(what) => write!(
81                f,
82                "document is encrypted, but its encryption header is damaged ({what})"
83            ),
84        }
85    }
86}
87
88impl std::error::Error for EncryptionError {}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn only_the_password_cases_need_a_password() {
96        assert!(EncryptionError::NeedPassword.needs_password());
97        assert!(EncryptionError::WrongPassword.needs_password());
98        assert!(!EncryptionError::NotDecryptable("ODF".into()).needs_password());
99        assert!(!EncryptionError::Malformed("keyBits".into()).needs_password());
100    }
101
102    #[test]
103    fn codes_are_distinct_and_stable() {
104        let all = [
105            EncryptionError::NeedPassword,
106            EncryptionError::WrongPassword,
107            EncryptionError::NotDecryptable("x".into()),
108            EncryptionError::Malformed("y".into()),
109        ];
110        let codes: std::collections::BTreeSet<_> = all.iter().map(|e| e.code()).collect();
111        assert_eq!(codes.len(), all.len());
112        assert_eq!(EncryptionError::NeedPassword.code(), "password_required");
113        assert_eq!(EncryptionError::WrongPassword.code(), "wrong_password");
114    }
115
116    /// The wording #624 fixed, which every Office backend prefixes with its
117    /// format name.
118    #[test]
119    fn display_is_the_office_wording() {
120        assert_eq!(
121            EncryptionError::NeedPassword.to_string(),
122            "document is encrypted (a password is required to open it)"
123        );
124        assert_eq!(
125            EncryptionError::WrongPassword.to_string(),
126            "document is encrypted and the password is wrong"
127        );
128        assert_eq!(
129            EncryptionError::NotDecryptable("XOR obfuscation".into()).to_string(),
130            "document is encrypted with an unsupported scheme (XOR obfuscation)"
131        );
132        assert_eq!(
133            EncryptionError::Malformed("keyBits".into()).to_string(),
134            "document is encrypted, but its encryption header is damaged (keyBits)"
135        );
136    }
137}