use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Pdf2MdError {
#[error("PDF file not found: '{path}'\nCheck the path exists and is readable.")]
FileNotFound { path: PathBuf },
#[error("Permission denied reading '{path}'\nTry: chmod +r {path:?}")]
PermissionDenied { path: PathBuf },
#[error("Invalid input '{input}': not a file path or a valid HTTP/HTTPS URL")]
InvalidInput { input: String },
#[error("Failed to download '{url}': {reason}\nCheck your internet connection.")]
DownloadFailed { url: String, reason: String },
#[error("Download timed out after {secs}s for '{url}'\nIncrease --download-timeout.")]
DownloadTimeout { url: String, secs: u64 },
#[error("File is not a valid PDF: '{path}'\nFirst bytes: {magic:?}")]
NotAPdf { path: PathBuf, magic: [u8; 4] },
#[error("PDF '{path}' is corrupt: {detail}\nTry repairing with: qpdf --decrypt input.pdf output.pdf")]
CorruptPdf { path: PathBuf, detail: String },
#[error("PDF '{path}' is encrypted and requires a password.\nProvide it with --password <PASSWORD>.")]
PasswordRequired { path: PathBuf },
#[error("Wrong password for PDF '{path}'")]
WrongPassword { path: PathBuf },
#[error("Page {page} is out of range (document has {total} pages)")]
PageOutOfRange { page: usize, total: usize },
#[error("Rasterisation failed for page {page}: {detail}")]
RasterisationFailed { page: usize, detail: String },
#[error("LLM provider '{provider}' is not configured.\n{hint}")]
ProviderNotConfigured { provider: String, hint: String },
#[error("LLM API error: {message}")]
LlmApiError { message: String },
#[error("All {total} pages failed after {retries} retries each.\nFirst error: {first_error}")]
AllPagesFailed {
total: usize,
retries: u32,
first_error: String,
},
#[error("{failed}/{total} pages failed during conversion")]
PartialFailure {
success: usize,
failed: usize,
total: usize,
},
#[error("Rate limit exceeded for provider '{provider}'")]
RateLimitExceeded {
provider: String,
retry_after_secs: Option<u64>,
},
#[error("API call timed out after {elapsed_ms}ms on page {page}")]
ApiTimeout { page: usize, elapsed_ms: u64 },
#[error("Authentication error from provider '{provider}': {detail}")]
AuthError { provider: String, detail: String },
#[error("Failed to write output file '{path}': {source}")]
OutputWriteFailed {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error(
"Failed to bind to pdfium library: {0}\n\n\
PDFium is normally downloaded automatically on first run.\n\
If the auto-download failed, you can:\n\
• Check your internet connection and try again.\n\
• Set PDFIUM_LIB_PATH=/path/to/libpdfium to use an existing copy.\n\
• Run `./scripts/setup-pdfium.sh` and set PDFIUM_LIB_PATH to the result.\n"
)]
PdfiumBindingFailed(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, Error, serde::Serialize, serde::Deserialize)]
pub enum PageError {
#[error("Page {page}: rasterisation failed: {detail}")]
RenderFailed { page: usize, detail: String },
#[error("Page {page}: LLM call failed after {retries} retries: {detail}")]
LlmFailed {
page: usize,
retries: u8,
detail: String,
},
#[error("Page {page}: LLM call timed out after {secs}s")]
Timeout { page: usize, secs: u64 },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_failure_display() {
let e = Pdf2MdError::PartialFailure {
success: 9,
failed: 1,
total: 10,
};
let msg = e.to_string();
assert!(msg.contains("1/10"), "got: {msg}");
}
#[test]
fn rate_limit_display_with_retry() {
let e = Pdf2MdError::RateLimitExceeded {
provider: "openai".into(),
retry_after_secs: Some(60),
};
assert!(e.to_string().contains("openai"));
}
#[test]
fn rate_limit_display_without_retry() {
let e = Pdf2MdError::RateLimitExceeded {
provider: "gemini".into(),
retry_after_secs: None,
};
assert!(e.to_string().contains("gemini"));
}
#[test]
fn api_timeout_display() {
let e = Pdf2MdError::ApiTimeout {
page: 3,
elapsed_ms: 5000,
};
assert!(e.to_string().contains("5000ms"));
assert!(e.to_string().contains("page 3"));
}
#[test]
fn auth_error_display() {
let e = Pdf2MdError::AuthError {
provider: "anthropic".into(),
detail: "invalid key".into(),
};
assert!(e.to_string().contains("anthropic"));
assert!(e.to_string().contains("invalid key"));
}
}