1use std::path::PathBuf;
16
17use crate::capability::Capability;
18use crate::compliance::{PdfAProfile, Violation};
19use crate::tier::Tier;
20
21pub type Result<T> = std::result::Result<T, Error>;
23
24#[derive(Debug)]
26#[non_exhaustive]
27pub enum Error {
28 Io {
31 source: std::io::Error,
33 path: Option<PathBuf>,
35 },
36 FileNotFound {
38 path: PathBuf,
40 },
41
42 InvalidPdf {
45 byte_offset: Option<u64>,
47 reason: String,
49 },
50 UnsupportedPdfVersion {
52 found: String,
54 supported_up_to: String,
56 },
57
58 PdfaValidationFailed {
61 profile: PdfAProfile,
63 violations: Vec<Violation>,
65 },
66
67 DecryptionFailed {
70 reason: DecryptionFailureReason,
72 },
73 InvalidSignature {
75 field: String,
77 reason: String,
79 },
80
81 FeatureNotInTier {
84 capability: Capability,
86 current_tier: Tier,
88 required_tier: Tier,
90 },
91 CapabilityNotCompiled {
93 capability: Capability,
95 feature_flag: &'static str,
97 },
98 InvalidLicense {
100 reason: String,
102 },
103 LicenseExpired {
109 expires_at: u64,
111 },
112 LicenseInvalidSignature,
119 LicenseRateLimited {
125 resource: String,
127 used: u64,
129 limit: u64,
131 },
132
133 UnsupportedOnWasm {
136 operation: &'static str,
138 },
139 MissingDependency {
141 dep: &'static str,
143 install_hint: &'static str,
145 },
146
147 MemoryBudgetExceeded {
150 requested: usize,
152 limit: usize,
154 },
155 ResourceLimitExceeded {
165 kind: ResourceLimitKind,
167 observed: u64,
169 limit: u64,
171 },
172
173 Unsupported(String),
176
177 Internal {
180 message: String,
182 crate_version: &'static str,
184 },
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189#[non_exhaustive]
190pub enum DecryptionFailureReason {
191 WrongPassword,
193 UnsupportedAlgorithm,
195 MalformedDictionary,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206#[non_exhaustive]
207pub enum ResourceLimitKind {
208 FileTooLarge,
211 StreamTooLarge,
214 ImageTooLarge,
217 ObjectDepthExceeded,
220 TooManyOperators,
223 XfaNestingTooDeep,
226 FormCalcRecursionTooDeep,
229}
230
231impl std::fmt::Display for ResourceLimitKind {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 Self::FileTooLarge => f.write_str("file too large"),
235 Self::StreamTooLarge => f.write_str("decompressed stream too large"),
236 Self::ImageTooLarge => f.write_str("image too large (pixel count)"),
237 Self::ObjectDepthExceeded => f.write_str("object reference depth exceeded"),
238 Self::TooManyOperators => f.write_str("content stream operator count exceeded"),
239 Self::XfaNestingTooDeep => f.write_str("XFA template nesting too deep"),
240 Self::FormCalcRecursionTooDeep => f.write_str("FormCalc recursion too deep"),
241 }
242 }
243}
244
245impl From<pdf_engine::LimitError> for Error {
246 fn from(e: pdf_engine::LimitError) -> Self {
247 use pdf_engine::LimitError as LE;
248 let (kind, observed, limit) = match e {
249 LE::FileTooLarge {
250 actual_bytes,
251 limit_bytes,
252 } => (ResourceLimitKind::FileTooLarge, actual_bytes, limit_bytes),
253 LE::StreamTooLarge {
254 actual_bytes,
255 limit_bytes,
256 } => (ResourceLimitKind::StreamTooLarge, actual_bytes, limit_bytes),
257 LE::ImageTooLarge {
258 pixels,
259 limit_pixels,
260 ..
261 } => (ResourceLimitKind::ImageTooLarge, pixels, limit_pixels),
262 LE::ObjectDepthExceeded { depth, limit } => (
263 ResourceLimitKind::ObjectDepthExceeded,
264 depth as u64,
265 limit as u64,
266 ),
267 LE::TooManyOperators { count, limit } => {
268 (ResourceLimitKind::TooManyOperators, count, limit)
269 }
270 LE::XfaNestingTooDeep { depth, limit } => (
271 ResourceLimitKind::XfaNestingTooDeep,
272 depth as u64,
273 limit as u64,
274 ),
275 LE::FormCalcRecursionTooDeep { depth, limit } => (
276 ResourceLimitKind::FormCalcRecursionTooDeep,
277 depth as u64,
278 limit as u64,
279 ),
280 };
281 Error::ResourceLimitExceeded {
282 kind,
283 observed,
284 limit,
285 }
286 }
287}
288
289impl std::fmt::Display for DecryptionFailureReason {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 match self {
292 Self::WrongPassword => f.write_str("wrong password"),
293 Self::UnsupportedAlgorithm => f.write_str("unsupported encryption algorithm"),
294 Self::MalformedDictionary => f.write_str("malformed encryption dictionary"),
295 }
296 }
297}
298
299impl Error {
300 pub const fn code(&self) -> &'static str {
316 match self {
317 Error::Io { .. } => "E-IO-GENERIC",
318 Error::FileNotFound { .. } => "E-IO-FILE-NOT-FOUND",
319 Error::InvalidPdf { .. } => "E-PARSE-INVALID-PDF",
320 Error::UnsupportedPdfVersion { .. } => "E-PARSE-UNSUPPORTED-VERSION",
321 Error::PdfaValidationFailed { .. } => "E-COMPLIANCE-PDFA-INVALID",
322 Error::DecryptionFailed { .. } => "E-SECURITY-DECRYPTION-FAILED",
323 Error::InvalidSignature { .. } => "E-SECURITY-INVALID-SIGNATURE",
324 Error::FeatureNotInTier { .. } => "E-LICENSE-FEATURE-NOT-IN-TIER",
325 Error::CapabilityNotCompiled { .. } => "E-LICENSE-CAPABILITY-NOT-COMPILED",
326 Error::InvalidLicense { .. } => "E-LICENSE-INVALID",
327 Error::LicenseExpired { .. } => "E-LICENSE-EXPIRED",
328 Error::LicenseInvalidSignature => "E-LICENSE-INVALID-SIGNATURE",
329 Error::LicenseRateLimited { .. } => "E-LICENSE-RATE-LIMITED",
330 Error::UnsupportedOnWasm { .. } => "E-ENV-UNSUPPORTED-ON-WASM",
331 Error::MissingDependency { .. } => "E-ENV-MISSING-DEPENDENCY",
332 Error::MemoryBudgetExceeded { .. } => "E-BUDGET-MEMORY-EXCEEDED",
333 Error::ResourceLimitExceeded { .. } => "E-BUDGET-RESOURCE-LIMIT",
334 Error::Unsupported(_) => "E-UNSUPPORTED",
335 Error::Internal { .. } => "E-INTERNAL",
336 }
337 }
338
339 pub const fn docs_url(&self) -> &'static str {
341 match self {
342 Error::Io { .. } => "https://pdfluent.com/errors/E-IO-GENERIC",
343 Error::FileNotFound { .. } => "https://pdfluent.com/errors/E-IO-FILE-NOT-FOUND",
344 Error::InvalidPdf { .. } => "https://pdfluent.com/errors/E-PARSE-INVALID-PDF",
345 Error::UnsupportedPdfVersion { .. } => {
346 "https://pdfluent.com/errors/E-PARSE-UNSUPPORTED-VERSION"
347 }
348 Error::PdfaValidationFailed { .. } => {
349 "https://pdfluent.com/errors/E-COMPLIANCE-PDFA-INVALID"
350 }
351 Error::DecryptionFailed { .. } => {
352 "https://pdfluent.com/errors/E-SECURITY-DECRYPTION-FAILED"
353 }
354 Error::InvalidSignature { .. } => {
355 "https://pdfluent.com/errors/E-SECURITY-INVALID-SIGNATURE"
356 }
357 Error::FeatureNotInTier { .. } => {
358 "https://pdfluent.com/errors/E-LICENSE-FEATURE-NOT-IN-TIER"
359 }
360 Error::CapabilityNotCompiled { .. } => {
361 "https://pdfluent.com/errors/E-LICENSE-CAPABILITY-NOT-COMPILED"
362 }
363 Error::InvalidLicense { .. } => "https://pdfluent.com/errors/E-LICENSE-INVALID",
364 Error::LicenseExpired { .. } => "https://pdfluent.com/errors/E-LICENSE-EXPIRED",
365 Error::LicenseInvalidSignature => {
366 "https://pdfluent.com/errors/E-LICENSE-INVALID-SIGNATURE"
367 }
368 Error::LicenseRateLimited { .. } => {
369 "https://pdfluent.com/errors/E-LICENSE-RATE-LIMITED"
370 }
371 Error::UnsupportedOnWasm { .. } => {
372 "https://pdfluent.com/errors/E-ENV-UNSUPPORTED-ON-WASM"
373 }
374 Error::MissingDependency { .. } => {
375 "https://pdfluent.com/errors/E-ENV-MISSING-DEPENDENCY"
376 }
377 Error::MemoryBudgetExceeded { .. } => {
378 "https://pdfluent.com/errors/E-BUDGET-MEMORY-EXCEEDED"
379 }
380 Error::ResourceLimitExceeded { .. } => {
381 "https://pdfluent.com/errors/E-BUDGET-RESOURCE-LIMIT"
382 }
383 Error::Unsupported(_) => "https://pdfluent.com/errors/E-UNSUPPORTED",
384 Error::Internal { .. } => "https://pdfluent.com/errors/E-INTERNAL",
385 }
386 }
387}
388
389impl std::fmt::Display for Error {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 match self {
392 Error::Io { source, path } => match path {
393 Some(p) => write!(f, "I/O error on {}: {source}", p.display()),
394 None => write!(f, "I/O error: {source}"),
395 },
396 Error::FileNotFound { path } => write!(f, "File not found: {}", path.display()),
397 Error::InvalidPdf { byte_offset, reason } => match byte_offset {
398 Some(o) => write!(f, "Invalid PDF at byte {o}: {reason}"),
399 None => write!(f, "Invalid PDF: {reason}"),
400 },
401 Error::UnsupportedPdfVersion { found, supported_up_to } => write!(
402 f,
403 "Unsupported PDF version {found} (this build supports up to {supported_up_to})"
404 ),
405 Error::PdfaValidationFailed { profile, violations } => write!(
406 f,
407 "PDF/A validation failed for profile {profile:?} with {} violation(s)",
408 violations.len()
409 ),
410 Error::DecryptionFailed { reason } => write!(f, "Decryption failed: {reason}"),
411 Error::InvalidSignature { field, reason } => {
412 write!(f, "Signature '{field}' is invalid: {reason}")
413 }
414 Error::FeatureNotInTier {
415 capability,
416 current_tier,
417 required_tier,
418 } => write!(
419 f,
420 "Capability {capability:?} requires tier {required_tier:?}; current tier is {current_tier:?}.\n Upgrade: https://pdfluent.com/pricing\n Docs: {}",
421 self.docs_url()
422 ),
423 Error::CapabilityNotCompiled {
424 capability,
425 feature_flag,
426 } => write!(
427 f,
428 "Capability {capability:?} requires the `{feature_flag}` Cargo feature, which is not enabled in this build.\n Docs: {}",
429 self.docs_url()
430 ),
431 Error::InvalidLicense { reason } => {
432 write!(f, "Invalid license: {reason}\n Docs: {}", self.docs_url())
433 }
434 Error::LicenseExpired { expires_at } => write!(
435 f,
436 "License expired at unix timestamp {expires_at}.\n Renew: https://pdfluent.com/pricing\n Docs: {}",
437 self.docs_url()
438 ),
439 Error::LicenseInvalidSignature => write!(
440 f,
441 "License signature does not verify against the configured public key — tampered or wrong-key payload.\n Docs: {}",
442 self.docs_url()
443 ),
444 Error::LicenseRateLimited {
445 resource,
446 used,
447 limit,
448 } => write!(
449 f,
450 "Rate limit exceeded: {used}/{limit} {resource} in the current window.\n Upgrade or wait for window reset.\n Docs: {}",
451 self.docs_url()
452 ),
453 Error::UnsupportedOnWasm { operation } => write!(
454 f,
455 "Operation `{operation}` is not supported on wasm32 targets.\n Docs: {}",
456 self.docs_url()
457 ),
458 Error::MissingDependency {
459 dep,
460 install_hint,
461 } => write!(
462 f,
463 "Missing dependency: {dep}.\n Install: {install_hint}\n Docs: {}",
464 self.docs_url()
465 ),
466 Error::MemoryBudgetExceeded { requested, limit } => write!(
467 f,
468 "Memory budget exceeded: requested {requested} bytes, limit is {limit}"
469 ),
470 Error::ResourceLimitExceeded {
471 kind,
472 observed,
473 limit,
474 } => write!(
475 f,
476 "Resource limit exceeded: {kind} (observed {observed}, limit {limit}).\n Docs: {}",
477 self.docs_url()
478 ),
479 Error::Unsupported(reason) => write!(f, "Unsupported operation: {reason}"),
480 Error::Internal { message, crate_version } => write!(
481 f,
482 "Internal error (please report): {message} [pdfluent {crate_version}]"
483 ),
484 }
485 }
486}
487
488impl std::error::Error for Error {
489 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
490 match self {
491 Error::Io { source, .. } => Some(source),
492 _ => None,
493 }
494 }
495}
496
497pub(crate) fn internal_error(message: impl Into<String>) -> Error {
506 Error::Internal {
507 message: message.into(),
508 crate_version: env!("CARGO_PKG_VERSION"),
509 }
510}
511
512impl From<pdf_engine::EngineError> for Error {
534 fn from(e: pdf_engine::EngineError) -> Self {
535 use pdf_engine::EngineError as E;
536 match e {
537 E::Encrypted(_reason) => Error::DecryptionFailed {
538 reason: DecryptionFailureReason::WrongPassword,
539 },
540 E::InvalidPdf(reason) => Error::InvalidPdf {
541 byte_offset: None,
542 reason,
543 },
544 E::LimitExceeded(le) => Error::from(le),
547 other => Error::InvalidPdf {
548 byte_offset: None,
549 reason: format!("{other:?}"),
550 },
551 }
552 }
553}
554
555impl From<lopdf::Error> for Error {
556 fn from(e: lopdf::Error) -> Self {
557 Error::InvalidPdf {
558 byte_offset: None,
559 reason: e.to_string(),
560 }
561 }
562}
563
564impl From<pdf_manip::ManipError> for Error {
565 fn from(e: pdf_manip::ManipError) -> Self {
566 use pdf_manip::ManipError as M;
567 match e {
568 M::DecryptionFailed => Error::DecryptionFailed {
569 reason: DecryptionFailureReason::WrongPassword,
570 },
571 other => Error::InvalidPdf {
572 byte_offset: None,
573 reason: other.to_string(),
574 },
575 }
576 }
577}
578
579impl From<pdf_sign::SignError> for Error {
580 fn from(e: pdf_sign::SignError) -> Self {
581 use pdf_sign::SignError as S;
582 match e {
583 S::Pkcs12Load(reason)
584 | S::UnsupportedKeyType(reason)
585 | S::CmsBuild(reason)
586 | S::SigningFailed(reason) => Error::InvalidSignature {
587 field: "<signing>".into(),
588 reason,
589 },
590 S::NoPrivateKey => Error::InvalidSignature {
591 field: "<signing>".into(),
592 reason: "PKCS#12 identity contained no private key".into(),
593 },
594 S::NoCertificate => Error::InvalidSignature {
595 field: "<signing>".into(),
596 reason: "PKCS#12 identity contained no certificate".into(),
597 },
598 }
599 }
600}
601
602impl From<pdf_redact::RedactError> for Error {
603 fn from(e: pdf_redact::RedactError) -> Self {
604 Error::InvalidPdf {
605 byte_offset: None,
606 reason: e.to_string(),
607 }
608 }
609}
610
611#[cfg(test)]
616mod tests {
617 use super::*;
618 use std::collections::HashSet;
619
620 #[test]
625 fn error_codes_are_unique() {
626 use std::path::PathBuf;
627
628 let variants: Vec<Error> = vec![
630 Error::Io {
631 source: std::io::Error::other("test"),
632 path: None,
633 },
634 Error::FileNotFound {
635 path: PathBuf::from("/tmp/test.pdf"),
636 },
637 Error::InvalidPdf {
638 byte_offset: None,
639 reason: "test".into(),
640 },
641 Error::UnsupportedPdfVersion {
642 found: "2.1".into(),
643 supported_up_to: "2.0".into(),
644 },
645 Error::PdfaValidationFailed {
646 profile: crate::compliance::PdfAProfile::A1b,
647 violations: vec![],
648 },
649 Error::DecryptionFailed {
650 reason: DecryptionFailureReason::WrongPassword,
651 },
652 Error::InvalidSignature {
653 field: "sig1".into(),
654 reason: "bad cert".into(),
655 },
656 Error::FeatureNotInTier {
657 capability: crate::capability::Capability::XfaFlatten,
658 current_tier: crate::tier::Tier::Trial,
659 required_tier: crate::tier::Tier::Developer,
660 },
661 Error::CapabilityNotCompiled {
662 capability: crate::capability::Capability::XfaFlatten,
663 feature_flag: "xfa",
664 },
665 Error::InvalidLicense {
666 reason: "expired".into(),
667 },
668 Error::LicenseExpired {
669 expires_at: 1_700_000_000,
670 },
671 Error::LicenseInvalidSignature,
672 Error::LicenseRateLimited {
673 resource: "api_calls".into(),
674 used: 1100,
675 limit: 1000,
676 },
677 Error::UnsupportedOnWasm { operation: "sign" },
678 Error::MissingDependency {
679 dep: "pdfium",
680 install_hint: "see README",
681 },
682 Error::MemoryBudgetExceeded {
683 requested: 1024,
684 limit: 512,
685 },
686 Error::ResourceLimitExceeded {
687 kind: ResourceLimitKind::FileTooLarge,
688 observed: 2000,
689 limit: 1000,
690 },
691 Error::Unsupported("test".into()),
692 Error::Internal {
693 message: "test".into(),
694 crate_version: "0.0.0",
695 },
696 ];
697
698 let mut seen: HashSet<&'static str> = HashSet::new();
699 for v in &variants {
700 let code = v.code();
701 assert!(seen.insert(code), "Duplicate error code detected: {code}");
702 }
703
704 assert_eq!(
706 variants.len(),
707 19,
708 "Update this test when new Error variants are added"
709 );
710 }
711
712 #[test]
714 fn docs_url_matches_code() {
715 use std::path::PathBuf;
716
717 let sample = Error::FileNotFound {
718 path: PathBuf::from("/tmp/x.pdf"),
719 };
720 let code = sample.code();
721 let url = sample.docs_url();
722 assert!(
723 url.ends_with(code),
724 "docs_url {url:?} must end with code {code:?}"
725 );
726 }
727}