pub mod adept;
mod epub_common;
pub mod ereader;
pub mod error;
pub mod ignoble;
pub mod kfx;
pub mod kobo;
pub mod mobipocket;
mod pdf_common;
pub mod topaz;
pub use error::SchemeError;
pub use flamberge_keys::KeyStore;
pub type Result<T> = std::result::Result<T, SchemeError>;
#[derive(Debug, Clone)]
pub struct DecryptedBook {
pub data: Vec<u8>,
pub extension: String,
pub title: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scheme {
Mobipocket,
Topaz,
Kfx,
AdeptEpub,
IgnobleEpub,
AdeptPdf,
IgnoblePdf,
EReader,
Kobo,
}
pub fn candidates_for_extension(ext: &str) -> &'static [Scheme] {
match ext.trim_start_matches('.').to_ascii_lowercase().as_str() {
"prc" | "mobi" | "pobi" | "azw" | "azw1" | "azw3" | "azw4" | "tpz" | "kfx-zip" => {
&[Scheme::Mobipocket, Scheme::Topaz, Scheme::Kfx]
}
"pdb" => &[Scheme::EReader],
"pdf" => &[Scheme::IgnoblePdf, Scheme::AdeptPdf],
"epub" => &[Scheme::IgnobleEpub, Scheme::AdeptEpub],
"kepub" => &[Scheme::Kobo],
_ => &[],
}
}
pub fn kindle_scheme_from_magic(data: &[u8]) -> Option<Scheme> {
if data.starts_with(b"\xeaDRMION\xee") {
None
} else if data.starts_with(b"PK\x03\x04") {
Some(Scheme::Kfx)
} else if data.starts_with(b"TPZ") {
Some(Scheme::Topaz)
} else {
Some(Scheme::Mobipocket)
}
}
pub fn decrypt(input: &[u8], ext: &str, keys: &KeyStore) -> Result<DecryptedBook> {
let mut candidates = candidates_for_extension(ext).to_vec();
let is_zip = input.starts_with(b"PK\x03\x04");
let is_kindle_family = candidates
.iter()
.any(|s| matches!(s, Scheme::Mobipocket | Scheme::Topaz | Scheme::Kfx));
if keys.kobo_db.is_some() && is_zip && !is_kindle_family && !candidates.contains(&Scheme::Kobo)
{
candidates.push(Scheme::Kobo);
}
if candidates.is_empty() {
return Err(SchemeError::UnknownFormat(ext.to_string()));
}
for scheme in candidates {
let result = match scheme {
Scheme::Mobipocket => mobipocket::decrypt(input, keys),
Scheme::Topaz => topaz::decrypt(input, keys),
Scheme::Kfx => kfx::decrypt(input, keys),
Scheme::AdeptEpub => adept::decrypt_epub(input, keys),
Scheme::IgnobleEpub => ignoble::decrypt_epub(input, keys),
Scheme::AdeptPdf => adept::decrypt_pdf(input, keys),
Scheme::IgnoblePdf => ignoble::decrypt_pdf(input, keys),
Scheme::EReader => ereader::decrypt(input, keys),
Scheme::Kobo => kobo::decrypt(input, keys),
};
match result {
Ok(book) => return Ok(book),
Err(SchemeError::NotThisScheme) => continue,
Err(e) => return Err(e),
}
}
Err(SchemeError::NoKeyWorked)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extension_routing_matches_plugin() {
assert_eq!(candidates_for_extension("mobi").len(), 3);
assert_eq!(candidates_for_extension(".AZW3").len(), 3); assert_eq!(candidates_for_extension("pdb"), &[Scheme::EReader]);
assert_eq!(
candidates_for_extension("pdf"),
&[Scheme::IgnoblePdf, Scheme::AdeptPdf]
);
assert_eq!(
candidates_for_extension("epub"),
&[Scheme::IgnobleEpub, Scheme::AdeptEpub]
);
assert_eq!(candidates_for_extension("kepub"), &[Scheme::Kobo]);
assert!(candidates_for_extension("txt").is_empty());
}
#[test]
fn unknown_extension_is_reported() {
let keys = KeyStore::new();
match decrypt(b"whatever", "txt", &keys) {
Err(SchemeError::UnknownFormat(ext)) => assert_eq!(ext, "txt"),
other => panic!("expected UnknownFormat, got {other:?}"),
}
}
#[test]
fn all_candidates_falling_through_yields_no_key_worked() {
let keys = KeyStore::new();
assert!(matches!(
decrypt(b"definitely not a zip", "epub", &keys),
Err(SchemeError::NoKeyWorked)
));
}
#[test]
fn terminal_error_is_surfaced_not_masked() {
let keys = KeyStore::new();
assert!(matches!(
decrypt(&[0u8; 8], "pdb", &keys),
Err(SchemeError::Format(_))
));
}
}