use std::io::Read;
use flamberge_crypto::des;
use flamberge_formats::palmdb::PalmDb;
use flamberge_formats::FormatError;
use super::header::Header;
use crate::{Result, SchemeError};
fn invalid(msg: impl Into<String>) -> SchemeError {
SchemeError::Format(FormatError::Invalid(msg.into()))
}
pub(super) fn de_xor(text: &[u8], sp: usize, table: &[u8]) -> Vec<u8> {
if table.is_empty() {
return text.to_vec();
}
let mut out = Vec::with_capacity(text.len());
let mut j = sp % table.len();
for &b in text {
out.push(table[j] ^ b);
j += 1;
if j == table.len() {
j = 0;
}
}
out
}
pub(super) fn clean_pml(pml: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(pml.len());
for &b in pml {
if b >= 0x80 {
out.extend_from_slice(format!("\\a{b:03}").as_bytes());
} else {
out.push(b);
}
}
out
}
fn zlib_inflate(data: &[u8]) -> Result<Vec<u8>> {
let mut out = Vec::new();
flate2::read::ZlibDecoder::new(data)
.read_to_end(&mut out)
.map_err(|_| invalid("eReader text record is not valid zlib"))?;
Ok(out)
}
fn page(content_key: &[u8], db: &PalmDb, data: &[u8], index: usize) -> Result<Vec<u8>> {
let record = db
.record(data, index)
.ok_or_else(|| invalid(format!("eReader page record {index} missing")))?;
let plain = des::ecb_decrypt(content_key, record)?;
zlib_inflate(&plain)
}
pub(super) fn extract_pml(
header: &Header,
db: &PalmDb,
data: &[u8],
content_key: &[u8],
) -> Result<Vec<u8>> {
let key = des::fix_key(content_key);
let mut out = Vec::new();
for i in 0..header.num_text_pages {
out.extend_from_slice(&page(&key, db, data, 1 + i)?);
}
if header.num_footnote_pages > 0 {
out.push(b'\n');
append_notes(
&mut out,
&key,
db,
data,
&header.xortable,
header.first_footnote_page,
header.num_footnote_pages,
"footnote",
)?;
}
if header.num_sidebar_pages > 0 {
out.push(b'\n');
append_notes(
&mut out,
&key,
db,
data,
&header.xortable,
header.first_sidebar_page,
header.num_sidebar_pages,
"sidebar",
)?;
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn append_notes(
out: &mut Vec<u8>,
key: &[u8],
db: &PalmDb,
data: &[u8],
xortable: &[u8],
first_page: usize,
num_pages: usize,
tag: &str,
) -> Result<()> {
let id_record = db
.record(data, first_page)
.ok_or_else(|| invalid(format!("eReader {tag} id record missing")))?;
let ids = de_xor(id_record, 0, xortable);
let mut cursor = 0usize;
for i in 1..num_pages {
let id_len = *ids
.get(cursor + 2)
.ok_or_else(|| invalid(format!("eReader {tag} id table truncated")))?
as usize;
let id = ids
.get(cursor + 3..cursor + 3 + id_len)
.ok_or_else(|| invalid(format!("eReader {tag} id table truncated")))?;
let id = String::from_utf8_lossy(id);
out.extend_from_slice(format!("<{tag} id=\"{id}\">\n").as_bytes());
out.extend_from_slice(&page(key, db, data, first_page + i)?);
out.extend_from_slice(format!("\n</{tag}>\n").as_bytes());
cursor += id_len + 4;
}
Ok(())
}
pub(super) fn extract_images(db: &PalmDb, data: &[u8], header: &Header) -> Vec<(String, Vec<u8>)> {
let mut images = Vec::new();
for i in 0..header.num_image_pages {
let Some(record) = db.record(data, header.first_image_page + i) else {
continue;
};
let Some(name_bytes) = record.get(4..36) else {
continue;
};
let name = sanitize_filename(name_bytes);
if name.is_empty() {
continue;
}
let body = record.get(62..).unwrap_or_default().to_vec();
images.push((name, body));
}
images
}
pub(super) fn sanitize_filename(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
bytes[..end]
.iter()
.filter(|&&b| b >= 0x20)
.map(|&b| match b {
b'<' => '[',
b'>' => ']',
b'/' | b'\\' | b'|' => '_',
b':' => '-',
b'"' => '\'',
_ => b as char, })
.collect::<String>()
.trim()
.trim_matches('.')
.to_string()
}