use base64::Engine;
use flamberge_keys::kobo::open_kobo_db;
use rusqlite::Connection;
use crate::{Result, SchemeError};
pub(super) struct WrappedKey {
pub elementid: String,
pub wrapped: Vec<u8>,
}
pub(super) struct VolumeKeys {
pub keys: Vec<WrappedKey>,
pub title: Option<String>,
}
pub(super) fn read_volume(db_bytes: &[u8], volumeid: Option<&str>) -> Result<VolumeKeys> {
let (_tmp, conn) = open_kobo_db(db_bytes.to_vec()).map_err(|e| invalid(e.to_string()))?;
let vid = match volumeid {
Some(v) => v.to_string(),
None => single_volume(&conn)?,
};
let keys = read_wrapped_keys(&conn, &vid)?;
if keys.is_empty() {
return Err(invalid(format!(
"the Kobo DB has no content keys for volume {vid}"
)));
}
let title = read_title(&conn, &vid);
Ok(VolumeKeys { keys, title })
}
fn single_volume(conn: &Connection) -> Result<String> {
let mut stmt = conn
.prepare(
"SELECT DISTINCT volumeid FROM content_keys, content \
WHERE volumeid = contentid",
)
.map_err(sqlite_err)?;
let mut volumes: Vec<String> = stmt
.query_map([], |row| row.get::<_, String>(0))
.map_err(sqlite_err)?
.collect::<rusqlite::Result<_>>()
.map_err(sqlite_err)?;
match volumes.len() {
1 => Ok(volumes.remove(0)),
0 => Err(invalid("the Kobo DB has no encrypted volumes")),
_ => Err(invalid(
"the Kobo DB has multiple volumes; specify the volume id",
)),
}
}
fn read_wrapped_keys(conn: &Connection, vid: &str) -> Result<Vec<WrappedKey>> {
let mut stmt = conn
.prepare(
"SELECT elementid, elementkey FROM content_keys, content \
WHERE volumeid = ?1 AND volumeid = contentid",
)
.map_err(sqlite_err)?;
let rows = stmt
.query_map([vid], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.map_err(sqlite_err)?;
let mut keys = Vec::new();
for row in rows {
let (elementid, elementkey_b64) = row.map_err(sqlite_err)?;
let wrapped = base64::engine::general_purpose::STANDARD
.decode(elementkey_b64.trim())
.map_err(|e| invalid(format!("invalid base64 page key for {elementid}: {e}")))?;
keys.push(WrappedKey { elementid, wrapped });
}
Ok(keys)
}
fn read_title(conn: &Connection, vid: &str) -> Option<String> {
conn.query_row(
"SELECT Title FROM content WHERE contentid = ?1",
[vid],
|row| row.get::<_, Option<String>>(0),
)
.ok()
.flatten()
.filter(|t| !t.is_empty())
}
fn sqlite_err(e: rusqlite::Error) -> SchemeError {
invalid(format!("Kobo DB: {e}"))
}
fn invalid(msg: impl Into<String>) -> SchemeError {
SchemeError::Format(flamberge_formats::FormatError::Invalid(msg.into()))
}