diff --git a/README.md b/README.md index cf8b548..9f02705 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ Inspired by [`imessage-exporter`](https://github.com/ReagentX/imessage-exporter), `crabapple` provides a flexible foundation for any project that needs to access iOS backup data. -## ⚠️ Warning ⚠️ - -This library is currently in beta state and should not be used in production code. - ## Features - Load and parse the backup's `Manifest.plist` to obtain metadata, device info, and encryption parameters @@ -35,10 +31,10 @@ fn main() -> Result<(), Box> { // Initialize a backup session for a device UDID with a password let udid_folder = "/Users/you/Library/Application Support/MobileSync/Backup/DEVICE_UDID"; let auth = Authentication::Password("your_password".into()); - let backup = Backup::new(udid_folder, &auth)?; + let backup = Backup::open(udid_folder, &auth)?; // List all files in the backup - let entries = backup.get_backup_files_list()?; + let entries = backup.entries()?; for entry in &entries { println!("{} - {}/{}", entry.file_id, entry.domain, entry.relative_path); } @@ -58,7 +54,7 @@ fn main() -> Result<(), Box> { } // Get the derived key for use elsewhere: - let derived_key = backup.get_decryption_key_hex(); + let derived_key = backup.decryption_key_hex(); Ok(()) } @@ -75,7 +71,7 @@ fn main() -> Result<(), Box> { let udid_folder = "/path/to/backup"; let hex_key = "abcdef0123456789..."; let auth = Authentication::DerivedKey(hex_key.to_string()); - let backup = Backup::new(udid_folder, &auth)?; + let backup = Backup::open(udid_folder, &auth)?; // ... proceed as normal Ok(()) } @@ -108,7 +104,7 @@ fn main() -> Result<(), Box> { let udid_folder = "/path/to/backup"; let hex_key = "abcdef0123456789..."; let auth = Authentication::DerivedKey(hex_key.to_string()); - let backup = Backup::new(udid_folder, &auth)?; + let backup = Backup::open(udid_folder, &auth)?; println!("Device: {} (iOS {})", backup.lockdown().device_name, @@ -127,13 +123,17 @@ fn main() -> Result<(), Box> { use crabapple::{Backup, Authentication}; use crabapple::error::BackupError; -match Backup::new("/bad/path", &Authentication::Password("pass".into())) { +match Backup::open("/bad/path", &Authentication::Password("pass".into())) { Ok(b) => println!("Loaded backup successfully"), Err(BackupError::ManifestPlistNotFound(path)) => eprintln!("Missing Manifest.plist: {}", path), Err(err) => eprintln!("Error initializing backup: {}", err), } ``` +## Targeted Versions + +This library targets the current latest public release for iOS. It should work with backups from iOS 10.2 or later, but all features may not be available. + ## Crabapple Tree ![My Crabapple Tree](/resources/crabapple.png) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index d0f5155..40df327 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -12,8 +12,6 @@ use std::{ path::{Path, PathBuf}, }; -use rusqlite::Connection; - use crate::{ backup::{ crypto::{AesCbcDecryptReader, aes_decrypt_cbc_with_padding, aes_kw_unwrap}, @@ -26,7 +24,7 @@ use crate::{ lockdown::ManifestLockdownInfo, manifest_plist::{Manifest, ManifestData}, }, - manifest_db::{ManifestDb, query_all_domains, query_all_files, query_file_by_id}, + manifest_db::ManifestDb, }, util::hex::hex_encode, }, @@ -37,6 +35,7 @@ use crate::{ /// /// Provides methods to initialize, configure, and extract data from a backup, /// including metadata loading, manifest database access, and file decryption. +#[derive(Debug)] pub struct Backup { /// Filesystem path to the specific device backup folder pub backup_path: PathBuf, @@ -44,8 +43,6 @@ pub struct Backup { pub manifest: Manifest, /// Decrypted manifest database details pub manifest_db: ManifestDb, - /// Connection to the manifest database - pub db: Connection, } impl Backup { @@ -64,7 +61,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -72,7 +69,7 @@ impl Backup { /// println!("UDID: {}", backup.udid()?); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn new>(backup_path: P, auth: &Authentication) -> Result { + pub fn open>(backup_path: P, auth: &Authentication) -> Result { let device_backup_path = backup_path.as_ref().to_path_buf(); if !device_backup_path.is_dir() { return Err(BackupError::InvalidBackupRoot( @@ -97,14 +94,10 @@ impl Backup { let manifest = Manifest::from_manifest_data(manifest_data, auth)?; let manifest_db = ManifestDb::new(&device_backup_path.join("Manifest.db"), &manifest)?; - // Create a connection to the manifest database - let conn = manifest_db.try_get_connection()?; - Ok(Self { backup_path: device_backup_path, manifest, manifest_db, - db: conn, }) } @@ -118,7 +111,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -144,7 +137,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -167,7 +160,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -189,7 +182,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -210,7 +203,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -220,7 +213,7 @@ impl Backup { /// println!("App: {}", app.bundle_id); /// } /// # Ok::<(), crabapple::error::BackupError>(()) - pub fn apps(&self) -> &Vec { + pub fn apps(&self) -> &[Application] { &self.manifest.manifest_data.applications } @@ -235,18 +228,18 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// if let Some(key_hex) = backup.get_decryption_key_hex() { + /// if let Some(key_hex) = backup.decryption_key_hex() { /// println!("Key: {}", key_hex); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` #[must_use] - pub fn get_decryption_key_hex(&self) -> Option { + pub fn decryption_key_hex(&self) -> Option { self.manifest .main_decryption_key .as_ref() @@ -263,18 +256,18 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// if let Some(key) = backup.get_decryption_key() { + /// if let Some(key) = backup.decryption_key() { /// println!("Key: {:?}", key); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` #[must_use] - pub fn get_decryption_key(&self) -> Option { + pub fn decryption_key(&self) -> Option { self.manifest.main_decryption_key.clone() } @@ -306,7 +299,7 @@ impl Backup { /// * `WirelessDomain` /// /// # Returns - /// A [`Vec`] containing each unique domain present in the backup. + /// A [`HashSet`] containing each unique domain present in the backup. /// /// # Errors /// Returns [`BackupError::ManifestDbNotFound`] if the manifest database is unavailable, @@ -317,7 +310,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -327,7 +320,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn query_all_domains(&self) -> Result> { - query_all_domains(&self.db) + self.manifest_db.query_all_domains() } /// Get the filesystem path to the decrypted (or raw) `Manifest.db` file. @@ -335,24 +328,21 @@ impl Backup { /// # Returns /// A [`Path`] pointing to the location of the manifest database file. /// - /// # Errors - /// Returns [`BackupError::ManifestDbNotFound`] if the manifest database information is missing. - /// /// # Examples /// /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// let db_path = backup.get_manifest_db_path(); + /// let db_path = backup.manifest_db_path(); /// println!("Manifest.db path: {:?}", db_path); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn get_manifest_db_path(&self) -> &Path { + pub fn manifest_db_path(&self) -> &Path { &self.manifest_db.db_path } @@ -366,19 +356,19 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// let files = backup.get_backup_files_list()?; - /// for file in files { - /// println!("{:?}", file); + /// let entries = backup.entries()?; + /// for entry in entries { + /// println!("{:?}", entry); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn get_backup_files_list(&self) -> Result> { - query_all_files(&self.db) + pub fn entries(&self) -> Result> { + self.manifest_db.query_all_entries() } /// Get a single file entry by its file ID. @@ -395,7 +385,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -405,21 +395,15 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn get_file(&self, file_id: &str) -> Result { - query_file_by_id(&self.db, file_id)? + self.manifest_db + .query_file_by_id(file_id)? .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) } - /// Access parsed `Manifest.plist` metadata. - #[must_use] - /// - /// # Returns - /// A reference to the parsed [`Manifest`] object. - pub fn manifest(&self) -> &ManifestData { - &self.manifest.manifest_data - } - /// Decrypt the file represented by [`BackupFileEntry`], returning plaintext bytes. /// + /// All operations are performed in memory, and the decrypted data is returned as a byte vector. + /// /// # Arguments /// * `entry` - A [`BackupFileEntry`] containing metadata and encrypted file ID. /// @@ -434,7 +418,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -445,28 +429,24 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn decrypt_entry(&self, entry: &BackupFileEntry) -> Result> { + if !self.is_encrypted() { + return Err(BackupError::NotEncrypted); + } + let source = self .backup_path .join(&entry.file_id[0..2]) .join(&entry.file_id); - let data = read(&source)?; + let ciphertext = read(&source)?; - // TODO: this is repeated in `Backup::decrypt_entry_stream`, clean it up - if let Some(encryption_key) = &entry.metadata.encryption_key { - let class_key_entry = self - .manifest - .get_class_key(entry.metadata.protection_class)?; - - let key = aes_kw_unwrap(&class_key_entry.key, &encryption_key.file_key)?; - - aes_decrypt_cbc_with_padding(&data, &key) - } else { - Ok(data) - } + let key = self.unwrap_key_for_entry(entry)?; + aes_decrypt_cbc_with_padding(&ciphertext, &key) } - /// Decrypt a file stream using AES-256-CBC with PKCS7 padding. + /// Decrypt the file represented by [`BackupFileEntry`], returning a streaming reader. + /// + /// All operations are streamed from the disk, and the decrypted data is returned as a reader. /// /// # Arguments /// * `entry` - A [`BackupFileEntry`] containing metadata and encrypted file ID. @@ -483,7 +463,7 @@ impl Backup { /// use std::{fs::File, io::copy}; /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -498,20 +478,41 @@ impl Backup { &self, entry: &BackupFileEntry, ) -> Result>> { - let ciphertext = File::open(self.backup_path.join(entry.source()))?; + if !self.is_encrypted() { + return Err(BackupError::NotEncrypted); + } - // TODO: this is repeated in `Backup::decrypt_entry`, clean it up - if let Some(encryption_key) = &entry.metadata.encryption_key { - let class_key_entry = self - .manifest - .get_class_key(entry.metadata.protection_class)?; + let ciphertext = File::open(self.backup_path.join(entry.source()))?; - let key = aes_kw_unwrap(&class_key_entry.key, &encryption_key.file_key)?; + let key = self.unwrap_key_for_entry(entry)?; + AesCbcDecryptReader::from(ciphertext, &key) + } - return AesCbcDecryptReader::from(ciphertext, &key); - } - Err(BackupError::KeyUnwrapFailed( - entry.metadata.protection_class, - )) + /// Unwrap the encryption key for a specific file entry. + /// + /// # Arguments + /// * `entry` - A [`BackupFileEntry`] containing metadata and encrypted file ID. + /// + /// # Returns + /// A streaming reader implementing `std::io::Read` that yields plaintext as it's read. + /// + /// # Errors + /// Returns [`BackupError::Crypto`] on decryption errors or missing keys. + fn unwrap_key_for_entry(&self, entry: &BackupFileEntry) -> Result { + let class_key_entry = self + .manifest + .get_class_key(entry.metadata.protection_class)?; + + let key = aes_kw_unwrap( + &class_key_entry.key, + &entry + .metadata + .encryption_key + .as_ref() + .ok_or(BackupError::NotEncrypted)? + .file_key, + )?; + + Ok(key) } } diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index 22014c8..c8485dd 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -23,6 +23,7 @@ use crate::{ }; /// Represents the backup's `Manifest.db`, decrypted if necessary, and holds decryption info. +#[derive(Debug)] pub struct ManifestDb { /// Path to the `SQLite` database file. pub db_path: PathBuf, @@ -32,6 +33,8 @@ pub struct ManifestDb { pub connection_string: String, /// Optional hex-encoded decryption key used to decrypt the database. pub decryption_key: Option, + /// Connection to the manifest database + pub conn: Option, } impl ManifestDb { @@ -50,12 +53,12 @@ impl ManifestDb { /// use crabapple::{Backup, Authentication}; /// use crabapple::backup::models::manifest_db; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()) /// )?; /// - /// let db_path = backup.get_manifest_db_path(); + /// let db_path = backup.manifest_db_path(); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn new(db_path: &Path, manifest: &Manifest) -> Result { @@ -64,16 +67,17 @@ impl ManifestDb { } let decrypted_db_info = if manifest.manifest_data.is_encrypted { - let manifest_key_bytes = manifest - .manifest_data - .manifest_key - .as_ref() - .ok_or_else(|| { - BackupError::Crypto( - "ManifestKey data not found in PlistInfo for encrypted Manifest.db" - .to_string(), - ) - })?; + let manifest_key_bytes = + manifest + .manifest_data + .manifest_key + .as_ref() + .ok_or_else(|| { + BackupError::Crypto( + "ManifestKey data not found in PlistInfo for encrypted Manifest.db" + .to_string(), + ) + })?; // The first 4 bytes of `manifest_key_bytes` are interpreted as a little-endian // `u32` protection class identifier. The remainder is treated as an AES-key-wrapped @@ -83,11 +87,9 @@ impl ManifestDb { // 2. Look up the corresponding unwrapped class key in `class_keys`. // 3. Unwrap the file-specific AES key using AES-Key-Wrap. // 4. Decrypt `ciphertext` with AES-256-CBC (zero IV), stripping PKCS#7 padding. - // TODO: this is repeated in `Backup::decrypt_entry`, clean it up let manifest_file_key = FileKeyPair::new(manifest_key_bytes)?; - let class_key_entry = - manifest.get_class_key(manifest_file_key.protection_class_id)?; + let class_key_entry = manifest.get_class_key(manifest_file_key.protection_class_id)?; let key = aes_kw_unwrap(&class_key_entry.key, &manifest_file_key.file_key) .map_err(|_| BackupError::KeyUnwrapFailed(manifest_file_key.protection_class_id))?; @@ -96,7 +98,7 @@ impl ManifestDb { let db_bytes = File::open(db_path)?; let mut decrypted_manifest_db_stream = AesCbcDecryptReader::from(&db_bytes, &key)?; - // Write decrypted Manifest.db into the platform-specific temporary directory + // Write decrypted Manifest.db into a platform-specific temporary directory let tmp_path = std::env::temp_dir().join("crabapple-Manifest.db"); let mut file = File::create(&tmp_path)?; @@ -106,10 +108,11 @@ impl ManifestDb { })?; Self { - db_path: tmp_path, + db_path: tmp_path.clone(), is_temporary: true, connection_string: db_path.to_string_lossy().into_owned(), // Path for direct open decryption_key: Some(hex_encode(&key)), + conn: Some(Connection::open(tmp_path).map_err(BackupError::Database)?), } } else { Self { @@ -117,190 +120,213 @@ impl ManifestDb { is_temporary: false, connection_string: db_path.to_string_lossy().into_owned(), decryption_key: None, + conn: Some(Connection::open(db_path).map_err(BackupError::Database)?), } }; Ok(decrypted_db_info) } - /// Open a `SQLite` connection to the manifest database. + /// Returns the current manifest database connection, if available. /// /// # Returns - /// A [`rusqlite::Connection`] to the database file specified by `db_path`. + /// An [`Result`] representing the current database connection. /// /// # Errors - /// Returns [`BackupError::Database`] if opening the connection fails. - pub fn try_get_connection(&self) -> Result { - rusqlite::Connection::open(&self.db_path).map_err(BackupError::Database) + /// Returns [`BackupError::DatabaseClosed`] if the manifest database connection is closed. + /// + /// # Examples + /// ```no_run + /// use crabapple::{Backup, Authentication}; + /// + /// let backup = Backup::open( + /// "/path/to/backup", + /// &Authentication::Password("pass".into()), + /// )?; + /// + /// let db = backup.manifest_db.db()?; + /// println!("Database connection: {:?}", db); + /// # Ok::<(), crabapple::error::BackupError>(()) + pub fn db(&self) -> Result<&Connection> { + self.conn.as_ref().ok_or(BackupError::DatabaseClosed) } -} -impl Drop for ManifestDb { - fn drop(&mut self) { - if self.is_temporary { - // Remove the file, ignoring errors if any - if let Err(e) = remove_file(&self.db_path) { - eprintln!( - "warning: failed to remove temporary `Manifest.db` file at {}: {}", - self.db_path.display(), - e - ); - } + /// Query all unique domains present in the `Manifest.db`. + /// + /// # Arguments + /// * `conn` - An open [`rusqlite::Connection`] to the manifest database. + /// + /// # Errors + /// Returns `BackupError::Database` on query failures. + /// + /// # Examples + /// + /// ```no_run + /// use crabapple::{Backup, Authentication}; + /// use crabapple::backup::models::manifest_db; + /// + /// let backup = Backup::open( + /// "/path/to/backup", + /// &Authentication::Password("pass".into()) + /// )?; + /// + /// let domains = backup.manifest_db.query_all_domains()?; + /// println!("Domains: {:?}", domains); + /// # Ok::<(), crabapple::error::BackupError>(()) + /// ``` + pub fn query_all_domains(&self) -> Result> { + let mut stmt = self.db()?.prepare( + "SELECT DISTINCT + CASE + WHEN INSTR(domain, '-') > 0 + THEN SUBSTR(domain, 1, INSTR(domain, '-') - 1) + ELSE + domain + END AS domain + FROM Files;", + )?; + let mut rows = stmt.query([])?; + let mut domains = HashSet::new(); + while let Some(row) = rows.next()? { + domains.insert(row.get(0)?); } + Ok(domains) } -} - -/// Query all unique domains present in the `Manifest.db`. -/// -/// # Arguments -/// * `conn` - An open [`rusqlite::Connection`] to the manifest database. -/// -/// # Errors -/// Returns `BackupError::Database` on query failures. -/// -/// # Examples -/// -/// ```no_run -/// use crabapple::{Backup, Authentication}; -/// use crabapple::backup::models::manifest_db; -/// -/// let backup = Backup::new( -/// "/path/to/backup", -/// &Authentication::Password("pass".into()) -/// )?; -/// -/// let domains = manifest_db::query_all_domains(&backup.db)?; -/// println!("Domains: {:?}", domains); -/// # Ok::<(), crabapple::error::BackupError>(()) -/// ``` -pub fn query_all_domains(conn: &Connection) -> Result> { - let mut stmt = conn.prepare( - "SELECT DISTINCT - CASE - WHEN INSTR(domain, '-') > 0 - THEN SUBSTR(domain, 1, INSTR(domain, '-') - 1) - ELSE - domain - END AS domain - FROM Files;", - )?; - let mut rows = stmt.query([])?; - let mut domains = HashSet::new(); - while let Some(row) = rows.next()? { - domains.insert(row.get(0)?); - } - Ok(domains) -} -/// Query all file entries from the `Manifest.db`. -/// -/// # Arguments -/// * `conn` - An open rusqlite `Connection`. -/// -/// # Errors -/// Returns [`BackupError::Database`] if the `SQL` query or blob reading fails. -/// -/// # Examples -/// -/// ```no_run -/// use crabapple::{Backup, Authentication}; -/// use crabapple::backup::models::manifest_db; -/// -/// let backup = Backup::new( -/// "/path/to/backup", -/// &Authentication::Password("pass".into()) -/// )?; -/// -/// let files = manifest_db::query_all_files(&backup.db)?; -/// println!("File count: {}", files.len()); -/// # Ok::<(), crabapple::error::BackupError>(()) -/// ``` -pub fn query_all_files(conn: &Connection) -> Result> { - let mut stmt = - conn.prepare("SELECT rowid, fileID, domain, relativePath, flags, file FROM Files")?; - let mut rows = stmt.query([])?; - let mut entries = Vec::new(); - while let Some(row) = rows.next()? { - let file_id = row.get(0)?; + /// Query all file entries from the `Manifest.db`. + /// + /// # Arguments + /// * `conn` - An open rusqlite `Connection`. + /// + /// # Errors + /// Returns [`BackupError::Database`] if the `SQL` query or blob reading fails. + /// + /// # Examples + /// + /// ```no_run + /// use crabapple::{Backup, Authentication}; + /// use crabapple::backup::models::manifest_db; + /// + /// let backup = Backup::open( + /// "/path/to/backup", + /// &Authentication::Password("pass".into()) + /// )?; + /// + /// let entries = backup.manifest_db.query_all_entries()?; + /// println!("File count: {}", entries.len()); + /// # Ok::<(), crabapple::error::BackupError>(()) + /// ``` + pub fn query_all_entries(&self) -> Result> { + let mut stmt = self + .db()? + .prepare("SELECT rowid, fileID, domain, relativePath, flags, file FROM Files")?; + let mut rows = stmt.query([])?; + let mut entries = Vec::new(); + while let Some(row) = rows.next()? { + let file_id = row.get(0)?; - let blob = conn - .blob_open(rusqlite::DatabaseName::Main, "Files", "file", file_id, true) - .map_err(BackupError::Database)?; + let blob = self + .db()? + .blob_open(rusqlite::DatabaseName::Main, "Files", "file", file_id, true) + .map_err(BackupError::Database)?; - let plist = Value::from_reader(blob) - .map_err(|_| BackupError::InvalidTlvData("Failed to parse file plist".to_string()))?; + let plist = Value::from_reader(blob).map_err(|_| { + BackupError::PlistParseError("Failed to parse `file` plist".to_string()) + })?; - let mbfile = MBFile::from_plist(&plist).map_err(|_| { - BackupError::InvalidTlvData("Failed to parse MBFile from plist".to_string()) - })?; + let mbfile = MBFile::from_plist(&plist).map_err(|_| { + BackupError::PlistParseError("Failed to parse `MBFile` from plist".to_string()) + })?; - entries.push(BackupFileEntry { - file_id: row.get(1)?, - domain: row.get(2)?, - relative_path: row.get(3)?, - flags: row.get(4)?, - metadata: mbfile, // Store the plist as metadata - }); + entries.push(BackupFileEntry { + file_id: row.get(1)?, + domain: row.get(2)?, + relative_path: row.get(3)?, + flags: row.get(4)?, + metadata: mbfile, // Store the plist as metadata + }); + } + Ok(entries) } - Ok(entries) -} -/// Query a single file entry by its file ID in the `Manifest.db`. -/// -/// # Arguments -/// * `conn` - An open rusqlite `Connection`. -/// * `path` - The `fileID` to look up in the `Files` table. -/// -/// # Returns -/// `Ok(Some(entry))` if found, `Ok(None)` if not found. -/// -/// # Errors -/// Returns [`BackupError::Database`] on query failures. -/// -/// # Examples -/// -/// ```no_run -/// use crabapple::{Backup, Authentication}; -/// use crabapple::backup::models::manifest_db; -/// -/// let backup = Backup::new( -/// "/path/to/backup", -/// &Authentication::Password("pass".into()) -/// )?; -/// -/// if let Some(entry) = manifest_db::query_file_by_id(&backup.db, "fileid")? { -/// println!("Found file: {}", entry.file_id); -/// } -/// # Ok::<(), crabapple::error::BackupError>(()) -/// ``` -pub fn query_file_by_id(conn: &Connection, path: &str) -> Result> { - // Path in DB is typically Domain-RelativePath - let mut stmt = conn.prepare( - "SELECT rowid, fileID, domain, relativePath, flags, file FROM Files WHERE fileID = ?", - )?; - let mut rows = stmt.query([path])?; - if let Some(row) = rows.next()? { - let file_id = row.get(0)?; + /// Query a single file entry by its file ID in the `Manifest.db`. + /// + /// # Arguments + /// * `conn` - An open rusqlite `Connection`. + /// * `path` - The `fileID` to look up in the `Files` table. + /// + /// # Returns + /// `Ok(Some(entry))` if found, `Ok(None)` if not found. + /// + /// # Errors + /// Returns [`BackupError::Database`] on query failures. + /// + /// # Examples + /// + /// ```no_run + /// use crabapple::{Backup, Authentication}; + /// use crabapple::backup::models::manifest_db; + /// + /// let backup = Backup::open( + /// "/path/to/backup", + /// &Authentication::Password("pass".into()) + /// )?; + /// + /// if let Some(entry) = backup.manifest_db.query_file_by_id("fileid")? { + /// println!("Found file: {}", entry.file_id); + /// } + /// # Ok::<(), crabapple::error::BackupError>(()) + /// ``` + pub fn query_file_by_id(&self, path: &str) -> Result> { + // Path in DB is typically Domain-RelativePath + let mut stmt = self.db()?.prepare( + "SELECT rowid, fileID, domain, relativePath, flags, file FROM Files WHERE fileID = ?", + )?; + let mut rows = stmt.query([path])?; + if let Some(row) = rows.next()? { + let file_id = row.get(0)?; + + let blob = self + .db()? + .blob_open(rusqlite::DatabaseName::Main, "Files", "file", file_id, true) + .map_err(BackupError::Database)?; + + let plist = Value::from_reader(blob).map_err(|_| { + BackupError::InvalidTlvData("Failed to parse file plist".to_string()) + })?; - let blob = conn - .blob_open(rusqlite::DatabaseName::Main, "Files", "file", file_id, true) - .map_err(BackupError::Database)?; + let mbfile = MBFile::from_plist(&plist).map_err(|_| { + BackupError::InvalidTlvData("Failed to parse MBFile from plist".to_string()) + })?; - let plist = Value::from_reader(blob) - .map_err(|_| BackupError::InvalidTlvData("Failed to parse file plist".to_string()))?; + Ok(Some(BackupFileEntry { + file_id: row.get(1)?, + domain: row.get(2)?, + relative_path: row.get(3)?, + flags: row.get(4)?, + metadata: mbfile, // Store the plist as metadata + })) + } else { + Ok(None) + } + } +} - let mbfile = MBFile::from_plist(&plist).map_err(|_| { - BackupError::InvalidTlvData("Failed to parse MBFile from plist".to_string()) - })?; +impl Drop for ManifestDb { + fn drop(&mut self) { + if self.is_temporary { + if let Some(conn) = self.conn.take() { + conn.close().ok(); - Ok(Some(BackupFileEntry { - file_id: row.get(1)?, - domain: row.get(2)?, - relative_path: row.get(3)?, - flags: row.get(4)?, - metadata: mbfile, // Store the plist as metadata - })) - } else { - Ok(None) + // Remove the file, ignoring errors if any + if let Err(e) = remove_file(&self.db_path) { + eprintln!( + "warning: failed to remove temporary `Manifest.db` file at {}: {}", + self.db_path.display(), + e + ); + } + } + } } } diff --git a/src/error.rs b/src/error.rs index c3971f5..d967377 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,9 @@ pub enum BackupError { /// `SQLite` database error (e.g., opening or querying Manifest.db). Database(rusqlite::Error), + /// The database was already closed + DatabaseClosed, + /// Cryptographic operation failed, with a descriptive message. Crypto(String), @@ -77,6 +80,7 @@ impl fmt::Display for BackupError { match self { BackupError::Io(err) => write!(f, "IO error: {err}"), BackupError::Database(err) => write!(f, "SQLite database error: {err}"), + BackupError::DatabaseClosed => write!(f, "Manifest.db was already closed!"), BackupError::Crypto(msg) => write!(f, "Cryptography error: {msg}"), BackupError::ConversionFailed(why) => { write!(f, "Conversion failed: {why}")