use std::path::PathBuf;
use crate::capability::Capability;
use crate::compliance::{PdfAProfile, Violation};
use crate::tier::Tier;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Io {
source: std::io::Error,
path: Option<PathBuf>,
},
FileNotFound {
path: PathBuf,
},
InvalidPdf {
byte_offset: Option<u64>,
reason: String,
},
UnsupportedPdfVersion {
found: String,
supported_up_to: String,
},
PdfaValidationFailed {
profile: PdfAProfile,
violations: Vec<Violation>,
},
DecryptionFailed {
reason: DecryptionFailureReason,
},
InvalidSignature {
field: String,
reason: String,
},
FeatureNotInTier {
capability: Capability,
current_tier: Tier,
required_tier: Tier,
},
CapabilityNotCompiled {
capability: Capability,
feature_flag: &'static str,
},
InvalidLicense {
reason: String,
},
LicenseExpired {
expires_at: u64,
},
LicenseInvalidSignature,
LicenseRateLimited {
resource: String,
used: u64,
limit: u64,
},
UnsupportedOnWasm {
operation: &'static str,
},
MissingDependency {
dep: &'static str,
install_hint: &'static str,
},
MemoryBudgetExceeded {
requested: usize,
limit: usize,
},
ResourceLimitExceeded {
kind: ResourceLimitKind,
observed: u64,
limit: u64,
},
Unsupported(String),
Internal {
message: String,
crate_version: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecryptionFailureReason {
WrongPassword,
UnsupportedAlgorithm,
MalformedDictionary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResourceLimitKind {
FileTooLarge,
StreamTooLarge,
ImageTooLarge,
ObjectDepthExceeded,
TooManyOperators,
XfaNestingTooDeep,
FormCalcRecursionTooDeep,
}
impl std::fmt::Display for ResourceLimitKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FileTooLarge => f.write_str("file too large"),
Self::StreamTooLarge => f.write_str("decompressed stream too large"),
Self::ImageTooLarge => f.write_str("image too large (pixel count)"),
Self::ObjectDepthExceeded => f.write_str("object reference depth exceeded"),
Self::TooManyOperators => f.write_str("content stream operator count exceeded"),
Self::XfaNestingTooDeep => f.write_str("XFA template nesting too deep"),
Self::FormCalcRecursionTooDeep => f.write_str("FormCalc recursion too deep"),
}
}
}
impl From<pdf_engine::LimitError> for Error {
fn from(e: pdf_engine::LimitError) -> Self {
use pdf_engine::LimitError as LE;
let (kind, observed, limit) = match e {
LE::FileTooLarge {
actual_bytes,
limit_bytes,
} => (ResourceLimitKind::FileTooLarge, actual_bytes, limit_bytes),
LE::StreamTooLarge {
actual_bytes,
limit_bytes,
} => (ResourceLimitKind::StreamTooLarge, actual_bytes, limit_bytes),
LE::ImageTooLarge {
pixels,
limit_pixels,
..
} => (ResourceLimitKind::ImageTooLarge, pixels, limit_pixels),
LE::ObjectDepthExceeded { depth, limit } => (
ResourceLimitKind::ObjectDepthExceeded,
depth as u64,
limit as u64,
),
LE::TooManyOperators { count, limit } => {
(ResourceLimitKind::TooManyOperators, count, limit)
}
LE::XfaNestingTooDeep { depth, limit } => (
ResourceLimitKind::XfaNestingTooDeep,
depth as u64,
limit as u64,
),
LE::FormCalcRecursionTooDeep { depth, limit } => (
ResourceLimitKind::FormCalcRecursionTooDeep,
depth as u64,
limit as u64,
),
};
Error::ResourceLimitExceeded {
kind,
observed,
limit,
}
}
}
impl std::fmt::Display for DecryptionFailureReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongPassword => f.write_str("wrong password"),
Self::UnsupportedAlgorithm => f.write_str("unsupported encryption algorithm"),
Self::MalformedDictionary => f.write_str("malformed encryption dictionary"),
}
}
}
impl Error {
pub const fn code(&self) -> &'static str {
match self {
Error::Io { .. } => "E-IO-GENERIC",
Error::FileNotFound { .. } => "E-IO-FILE-NOT-FOUND",
Error::InvalidPdf { .. } => "E-PARSE-INVALID-PDF",
Error::UnsupportedPdfVersion { .. } => "E-PARSE-UNSUPPORTED-VERSION",
Error::PdfaValidationFailed { .. } => "E-COMPLIANCE-PDFA-INVALID",
Error::DecryptionFailed { .. } => "E-SECURITY-DECRYPTION-FAILED",
Error::InvalidSignature { .. } => "E-SECURITY-INVALID-SIGNATURE",
Error::FeatureNotInTier { .. } => "E-LICENSE-FEATURE-NOT-IN-TIER",
Error::CapabilityNotCompiled { .. } => "E-LICENSE-CAPABILITY-NOT-COMPILED",
Error::InvalidLicense { .. } => "E-LICENSE-INVALID",
Error::LicenseExpired { .. } => "E-LICENSE-EXPIRED",
Error::LicenseInvalidSignature => "E-LICENSE-INVALID-SIGNATURE",
Error::LicenseRateLimited { .. } => "E-LICENSE-RATE-LIMITED",
Error::UnsupportedOnWasm { .. } => "E-ENV-UNSUPPORTED-ON-WASM",
Error::MissingDependency { .. } => "E-ENV-MISSING-DEPENDENCY",
Error::MemoryBudgetExceeded { .. } => "E-BUDGET-MEMORY-EXCEEDED",
Error::ResourceLimitExceeded { .. } => "E-BUDGET-RESOURCE-LIMIT",
Error::Unsupported(_) => "E-UNSUPPORTED",
Error::Internal { .. } => "E-INTERNAL",
}
}
pub const fn docs_url(&self) -> &'static str {
match self {
Error::Io { .. } => "https://pdfluent.com/errors/E-IO-GENERIC",
Error::FileNotFound { .. } => "https://pdfluent.com/errors/E-IO-FILE-NOT-FOUND",
Error::InvalidPdf { .. } => "https://pdfluent.com/errors/E-PARSE-INVALID-PDF",
Error::UnsupportedPdfVersion { .. } => {
"https://pdfluent.com/errors/E-PARSE-UNSUPPORTED-VERSION"
}
Error::PdfaValidationFailed { .. } => {
"https://pdfluent.com/errors/E-COMPLIANCE-PDFA-INVALID"
}
Error::DecryptionFailed { .. } => {
"https://pdfluent.com/errors/E-SECURITY-DECRYPTION-FAILED"
}
Error::InvalidSignature { .. } => {
"https://pdfluent.com/errors/E-SECURITY-INVALID-SIGNATURE"
}
Error::FeatureNotInTier { .. } => {
"https://pdfluent.com/errors/E-LICENSE-FEATURE-NOT-IN-TIER"
}
Error::CapabilityNotCompiled { .. } => {
"https://pdfluent.com/errors/E-LICENSE-CAPABILITY-NOT-COMPILED"
}
Error::InvalidLicense { .. } => "https://pdfluent.com/errors/E-LICENSE-INVALID",
Error::LicenseExpired { .. } => "https://pdfluent.com/errors/E-LICENSE-EXPIRED",
Error::LicenseInvalidSignature => {
"https://pdfluent.com/errors/E-LICENSE-INVALID-SIGNATURE"
}
Error::LicenseRateLimited { .. } => {
"https://pdfluent.com/errors/E-LICENSE-RATE-LIMITED"
}
Error::UnsupportedOnWasm { .. } => {
"https://pdfluent.com/errors/E-ENV-UNSUPPORTED-ON-WASM"
}
Error::MissingDependency { .. } => {
"https://pdfluent.com/errors/E-ENV-MISSING-DEPENDENCY"
}
Error::MemoryBudgetExceeded { .. } => {
"https://pdfluent.com/errors/E-BUDGET-MEMORY-EXCEEDED"
}
Error::ResourceLimitExceeded { .. } => {
"https://pdfluent.com/errors/E-BUDGET-RESOURCE-LIMIT"
}
Error::Unsupported(_) => "https://pdfluent.com/errors/E-UNSUPPORTED",
Error::Internal { .. } => "https://pdfluent.com/errors/E-INTERNAL",
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io { source, path } => match path {
Some(p) => write!(f, "I/O error on {}: {source}", p.display()),
None => write!(f, "I/O error: {source}"),
},
Error::FileNotFound { path } => write!(f, "File not found: {}", path.display()),
Error::InvalidPdf { byte_offset, reason } => match byte_offset {
Some(o) => write!(f, "Invalid PDF at byte {o}: {reason}"),
None => write!(f, "Invalid PDF: {reason}"),
},
Error::UnsupportedPdfVersion { found, supported_up_to } => write!(
f,
"Unsupported PDF version {found} (this build supports up to {supported_up_to})"
),
Error::PdfaValidationFailed { profile, violations } => write!(
f,
"PDF/A validation failed for profile {profile:?} with {} violation(s)",
violations.len()
),
Error::DecryptionFailed { reason } => write!(f, "Decryption failed: {reason}"),
Error::InvalidSignature { field, reason } => {
write!(f, "Signature '{field}' is invalid: {reason}")
}
Error::FeatureNotInTier {
capability,
current_tier,
required_tier,
} => write!(
f,
"Capability {capability:?} requires tier {required_tier:?}; current tier is {current_tier:?}.\n Upgrade: https://pdfluent.com/pricing\n Docs: {}",
self.docs_url()
),
Error::CapabilityNotCompiled {
capability,
feature_flag,
} => write!(
f,
"Capability {capability:?} requires the `{feature_flag}` Cargo feature, which is not enabled in this build.\n Docs: {}",
self.docs_url()
),
Error::InvalidLicense { reason } => {
write!(f, "Invalid license: {reason}\n Docs: {}", self.docs_url())
}
Error::LicenseExpired { expires_at } => write!(
f,
"License expired at unix timestamp {expires_at}.\n Renew: https://pdfluent.com/pricing\n Docs: {}",
self.docs_url()
),
Error::LicenseInvalidSignature => write!(
f,
"License signature does not verify against the configured public key — tampered or wrong-key payload.\n Docs: {}",
self.docs_url()
),
Error::LicenseRateLimited {
resource,
used,
limit,
} => write!(
f,
"Rate limit exceeded: {used}/{limit} {resource} in the current window.\n Upgrade or wait for window reset.\n Docs: {}",
self.docs_url()
),
Error::UnsupportedOnWasm { operation } => write!(
f,
"Operation `{operation}` is not supported on wasm32 targets.\n Docs: {}",
self.docs_url()
),
Error::MissingDependency {
dep,
install_hint,
} => write!(
f,
"Missing dependency: {dep}.\n Install: {install_hint}\n Docs: {}",
self.docs_url()
),
Error::MemoryBudgetExceeded { requested, limit } => write!(
f,
"Memory budget exceeded: requested {requested} bytes, limit is {limit}"
),
Error::ResourceLimitExceeded {
kind,
observed,
limit,
} => write!(
f,
"Resource limit exceeded: {kind} (observed {observed}, limit {limit}).\n Docs: {}",
self.docs_url()
),
Error::Unsupported(reason) => write!(f, "Unsupported operation: {reason}"),
Error::Internal { message, crate_version } => write!(
f,
"Internal error (please report): {message} [pdfluent {crate_version}]"
),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } => Some(source),
_ => None,
}
}
}
pub(crate) fn internal_error(message: impl Into<String>) -> Error {
Error::Internal {
message: message.into(),
crate_version: env!("CARGO_PKG_VERSION"),
}
}
impl From<pdf_engine::EngineError> for Error {
fn from(e: pdf_engine::EngineError) -> Self {
use pdf_engine::EngineError as E;
match e {
E::Encrypted(_reason) => Error::DecryptionFailed {
reason: DecryptionFailureReason::WrongPassword,
},
E::InvalidPdf(reason) => Error::InvalidPdf {
byte_offset: None,
reason,
},
E::LimitExceeded(le) => Error::from(le),
other => Error::InvalidPdf {
byte_offset: None,
reason: format!("{other:?}"),
},
}
}
}
impl From<lopdf::Error> for Error {
fn from(e: lopdf::Error) -> Self {
Error::InvalidPdf {
byte_offset: None,
reason: e.to_string(),
}
}
}
impl From<pdf_manip::ManipError> for Error {
fn from(e: pdf_manip::ManipError) -> Self {
use pdf_manip::ManipError as M;
match e {
M::DecryptionFailed => Error::DecryptionFailed {
reason: DecryptionFailureReason::WrongPassword,
},
other => Error::InvalidPdf {
byte_offset: None,
reason: other.to_string(),
},
}
}
}
impl From<pdf_sign::SignError> for Error {
fn from(e: pdf_sign::SignError) -> Self {
use pdf_sign::SignError as S;
match e {
S::Pkcs12Load(reason)
| S::UnsupportedKeyType(reason)
| S::CmsBuild(reason)
| S::SigningFailed(reason) => Error::InvalidSignature {
field: "<signing>".into(),
reason,
},
S::NoPrivateKey => Error::InvalidSignature {
field: "<signing>".into(),
reason: "PKCS#12 identity contained no private key".into(),
},
S::NoCertificate => Error::InvalidSignature {
field: "<signing>".into(),
reason: "PKCS#12 identity contained no certificate".into(),
},
}
}
}
impl From<pdf_redact::RedactError> for Error {
fn from(e: pdf_redact::RedactError) -> Self {
Error::InvalidPdf {
byte_offset: None,
reason: e.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn error_codes_are_unique() {
use std::path::PathBuf;
let variants: Vec<Error> = vec![
Error::Io {
source: std::io::Error::other("test"),
path: None,
},
Error::FileNotFound {
path: PathBuf::from("/tmp/test.pdf"),
},
Error::InvalidPdf {
byte_offset: None,
reason: "test".into(),
},
Error::UnsupportedPdfVersion {
found: "2.1".into(),
supported_up_to: "2.0".into(),
},
Error::PdfaValidationFailed {
profile: crate::compliance::PdfAProfile::A1b,
violations: vec![],
},
Error::DecryptionFailed {
reason: DecryptionFailureReason::WrongPassword,
},
Error::InvalidSignature {
field: "sig1".into(),
reason: "bad cert".into(),
},
Error::FeatureNotInTier {
capability: crate::capability::Capability::XfaFlatten,
current_tier: crate::tier::Tier::Trial,
required_tier: crate::tier::Tier::Developer,
},
Error::CapabilityNotCompiled {
capability: crate::capability::Capability::XfaFlatten,
feature_flag: "xfa",
},
Error::InvalidLicense {
reason: "expired".into(),
},
Error::LicenseExpired {
expires_at: 1_700_000_000,
},
Error::LicenseInvalidSignature,
Error::LicenseRateLimited {
resource: "api_calls".into(),
used: 1100,
limit: 1000,
},
Error::UnsupportedOnWasm { operation: "sign" },
Error::MissingDependency {
dep: "pdfium",
install_hint: "see README",
},
Error::MemoryBudgetExceeded {
requested: 1024,
limit: 512,
},
Error::ResourceLimitExceeded {
kind: ResourceLimitKind::FileTooLarge,
observed: 2000,
limit: 1000,
},
Error::Unsupported("test".into()),
Error::Internal {
message: "test".into(),
crate_version: "0.0.0",
},
];
let mut seen: HashSet<&'static str> = HashSet::new();
for v in &variants {
let code = v.code();
assert!(seen.insert(code), "Duplicate error code detected: {code}");
}
assert_eq!(
variants.len(),
19,
"Update this test when new Error variants are added"
);
}
#[test]
fn docs_url_matches_code() {
use std::path::PathBuf;
let sample = Error::FileNotFound {
path: PathBuf::from("/tmp/x.pdf"),
};
let code = sample.code();
let url = sample.docs_url();
assert!(
url.ends_with(code),
"docs_url {url:?} must end with code {code:?}"
);
}
}