1pub mod adept;
8mod epub_common;
9pub mod ereader;
10pub mod error;
11pub mod ignoble;
12pub mod kfx;
13pub mod kobo;
14pub mod mobipocket;
15mod pdf_common;
16pub mod topaz;
17
18pub use error::SchemeError;
19pub use flamberge_keys::KeyStore;
20
21pub type Result<T> = std::result::Result<T, SchemeError>;
22
23#[derive(Debug, Clone)]
25pub struct DecryptedBook {
26 pub data: Vec<u8>,
27 pub extension: String,
29 pub title: Option<String>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Scheme {
37 Mobipocket,
38 Topaz,
39 Kfx,
40 AdeptEpub,
41 IgnobleEpub,
42 AdeptPdf,
43 IgnoblePdf,
44 EReader,
45 Kobo,
46}
47
48pub fn candidates_for_extension(ext: &str) -> &'static [Scheme] {
53 match ext.trim_start_matches('.').to_ascii_lowercase().as_str() {
54 "prc" | "mobi" | "pobi" | "azw" | "azw1" | "azw3" | "azw4" | "tpz" | "kfx-zip" => {
55 &[Scheme::Mobipocket, Scheme::Topaz, Scheme::Kfx]
56 }
57 "pdb" => &[Scheme::EReader],
58 "pdf" => &[Scheme::IgnoblePdf, Scheme::AdeptPdf],
59 "epub" => &[Scheme::IgnobleEpub, Scheme::AdeptEpub],
60 "kepub" => &[Scheme::Kobo],
61 _ => &[],
62 }
63}
64
65pub fn kindle_scheme_from_magic(data: &[u8]) -> Option<Scheme> {
68 if data.starts_with(b"\xeaDRMION\xee") {
69 None
71 } else if data.starts_with(b"PK\x03\x04") {
72 Some(Scheme::Kfx)
73 } else if data.starts_with(b"TPZ") {
74 Some(Scheme::Topaz)
75 } else {
76 Some(Scheme::Mobipocket)
77 }
78}
79
80pub fn decrypt(input: &[u8], ext: &str, keys: &KeyStore) -> Result<DecryptedBook> {
83 let mut candidates = candidates_for_extension(ext).to_vec();
84
85 let is_zip = input.starts_with(b"PK\x03\x04");
92 let is_kindle_family = candidates
93 .iter()
94 .any(|s| matches!(s, Scheme::Mobipocket | Scheme::Topaz | Scheme::Kfx));
95 if keys.kobo_db.is_some() && is_zip && !is_kindle_family && !candidates.contains(&Scheme::Kobo)
96 {
97 candidates.push(Scheme::Kobo);
98 }
99
100 if candidates.is_empty() {
101 return Err(SchemeError::UnknownFormat(ext.to_string()));
102 }
103
104 for scheme in candidates {
105 let result = match scheme {
106 Scheme::Mobipocket => mobipocket::decrypt(input, keys),
107 Scheme::Topaz => topaz::decrypt(input, keys),
108 Scheme::Kfx => kfx::decrypt(input, keys),
109 Scheme::AdeptEpub => adept::decrypt_epub(input, keys),
110 Scheme::IgnobleEpub => ignoble::decrypt_epub(input, keys),
111 Scheme::AdeptPdf => adept::decrypt_pdf(input, keys),
112 Scheme::IgnoblePdf => ignoble::decrypt_pdf(input, keys),
113 Scheme::EReader => ereader::decrypt(input, keys),
114 Scheme::Kobo => kobo::decrypt(input, keys),
115 };
116 match result {
121 Ok(book) => return Ok(book),
122 Err(SchemeError::NotThisScheme) => continue,
123 Err(e) => return Err(e),
124 }
125 }
126 Err(SchemeError::NoKeyWorked)
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn extension_routing_matches_plugin() {
136 assert_eq!(candidates_for_extension("mobi").len(), 3);
137 assert_eq!(candidates_for_extension(".AZW3").len(), 3); assert_eq!(candidates_for_extension("pdb"), &[Scheme::EReader]);
139 assert_eq!(
140 candidates_for_extension("pdf"),
141 &[Scheme::IgnoblePdf, Scheme::AdeptPdf]
142 );
143 assert_eq!(
144 candidates_for_extension("epub"),
145 &[Scheme::IgnobleEpub, Scheme::AdeptEpub]
146 );
147 assert_eq!(candidates_for_extension("kepub"), &[Scheme::Kobo]);
148 assert!(candidates_for_extension("txt").is_empty());
149 }
150
151 #[test]
152 fn unknown_extension_is_reported() {
153 let keys = KeyStore::new();
154 match decrypt(b"whatever", "txt", &keys) {
155 Err(SchemeError::UnknownFormat(ext)) => assert_eq!(ext, "txt"),
156 other => panic!("expected UnknownFormat, got {other:?}"),
157 }
158 }
159
160 #[test]
161 fn all_candidates_falling_through_yields_no_key_worked() {
162 let keys = KeyStore::new();
166 assert!(matches!(
167 decrypt(b"definitely not a zip", "epub", &keys),
168 Err(SchemeError::NoKeyWorked)
169 ));
170 }
171
172 #[test]
173 fn terminal_error_is_surfaced_not_masked() {
174 let keys = KeyStore::new();
179 assert!(matches!(
180 decrypt(&[0u8; 8], "pdb", &keys),
181 Err(SchemeError::Format(_))
182 ));
183 }
184}