Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS cli option #113

Merged
merged 18 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 2 additions & 7 deletions imessage-database/src/tables/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,8 @@ impl Table for Attachment {

fn extract(attachment: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
match attachment {
Ok(attachment) => match attachment {
Ok(att) => Ok(att),
// TODO: When does this occur?
Err(why) => Err(TableError::Attachment(why)),
},
// TODO: When does this occur?
Err(why) => Err(TableError::Attachment(why)),
Ok(Ok(attachment)) => Ok(attachment),
Err(why) | Ok(Err(why)) => Err(TableError::Attachment(why)),
}
}
}
Expand Down
9 changes: 2 additions & 7 deletions imessage-database/src/tables/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,8 @@ impl Table for Chat {

fn extract(chat: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
match chat {
Ok(chat) => match chat {
Ok(ch) => Ok(ch),
// TODO: When does this occur?
Err(why) => Err(TableError::Chat(why)),
},
// TODO: When does this occur?
Err(why) => Err(TableError::Chat(why)),
Ok(Ok(chat)) => Ok(chat),
Err(why) | Ok(Err(why)) => Err(TableError::Chat(why)),
}
}
}
Expand Down
9 changes: 2 additions & 7 deletions imessage-database/src/tables/chat_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,8 @@ impl Table for ChatToHandle {

fn extract(chat_to_handle: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
match chat_to_handle {
Ok(chat_to_handle) => match chat_to_handle {
Ok(c2h) => Ok(c2h),
// TODO: When does this occur?
Err(why) => Err(TableError::ChatToHandle(why)),
},
// TODO: When does this occur?
Err(why) => Err(TableError::ChatToHandle(why)),
Ok(Ok(chat_to_handle)) => Ok(chat_to_handle),
Err(why) | Ok(Err(why)) => Err(TableError::ChatToHandle(why)),
}
}
}
Expand Down
9 changes: 2 additions & 7 deletions imessage-database/src/tables/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,8 @@ impl Table for Handle {

fn extract(handle: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
match handle {
Ok(handle) => match handle {
Ok(hdl) => Ok(hdl),
// TODO: When does this occur?
Err(why) => Err(TableError::Handle(why)),
},
// TODO: When does this occur?
Err(why) => Err(TableError::Handle(why)),
Ok(Ok(handle)) => Ok(handle),
Err(why) | Ok(Err(why)) => Err(TableError::Handle(why)),
}
}
}
Expand Down
9 changes: 2 additions & 7 deletions imessage-database/src/tables/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,8 @@ impl Table for Message {

fn extract(message: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
match message {
Ok(message) => match message {
Ok(msg) => Ok(msg),
// TODO: When does this occur?
Err(why) => Err(TableError::Messages(why)),
},
// TODO: When does this occur?
Err(why) => Err(TableError::Messages(why)),
Ok(Ok(message)) => Ok(message),
Err(why) | Ok(Err(why)) => Err(TableError::Messages(why)),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions imessage-exporter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ imessage-database = {path = "../imessage-database"}
indicatif = "0.17.3"
rusqlite = {version = "0.29.0", features = ["blob", "bundled"]}
uuid = {version = "1.3.0", features = ["v4", "fast-rng"]}
sha1 = "0.10.5"
28 changes: 28 additions & 0 deletions imessage-exporter/src/app/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use crate::app::error::RuntimeError;

/// Default export directory name
pub const DEFAULT_OUTPUT_DIR: &str = "imessage_export";
/// Default location in an iOS backup to find the iMessage database
pub const DEFAULT_IOS_CHATDB_PATH: &str = "3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28";
/// Default location in an iOS backup to find the iMessage contacts database
pub const DEFAULT_IOS_CONTACTSDB_PATH: &str = "31/31bb7ba8914766d4ba40d6dfb6113c8b614be442"; // unused
ReagentX marked this conversation as resolved.
Show resolved Hide resolved

// CLI Arg Names
pub const OPTION_DB_PATH: &str = "db-path";
Expand All @@ -22,6 +26,7 @@ pub const OPTION_START_DATE: &str = "start-date";
pub const OPTION_END_DATE: &str = "end-date";
pub const OPTION_DISABLE_LAZY_LOADING: &str = "no-lazy";
pub const OPTION_CUSTOM_NAME: &str = "custom-name";
pub const OPTION_IOS: &str = "ios";

// Other CLI Text
pub const SUPPORTED_FILE_TYPES: &str = "txt, html";
Expand All @@ -48,6 +53,8 @@ pub struct Options<'a> {
pub no_lazy: bool,
/// Custom name for database owner in output
pub custom_name: Option<&'a str>,
/// If true, enable iOS-specific features, db_path is to a backup, uses hashed filepaths
pub ios: bool,
}

impl<'a> Options<'a> {
Expand All @@ -61,6 +68,7 @@ impl<'a> Options<'a> {
let end_date = args.value_of(OPTION_END_DATE);
let no_lazy = args.is_present(OPTION_DISABLE_LAZY_LOADING);
let custom_name = args.value_of(OPTION_CUSTOM_NAME);
let ios = args.is_present(OPTION_IOS);

// Ensure export type is allowed
if let Some(found_type) = export_type {
Expand Down Expand Up @@ -108,6 +116,18 @@ impl<'a> Options<'a> {
)));
}

// Ensure that if iOS is enabled, that the db_path is to a backup
if ios && user_path.is_some() {
let db_path = PathBuf::from(user_path.unwrap());
if !db_path.join(DEFAULT_IOS_CHATDB_PATH).exists() {
return Err(RuntimeError::InvalidOptions(format!(
"Option {OPTION_IOS} is enabled, but the database path does not appear to be a valid iOS backup"
ReagentX marked this conversation as resolved.
Show resolved Hide resolved
)));
}
}



// Build query context
let mut query_context = QueryContext::default();
if let Some(start) = start_date {
Expand Down Expand Up @@ -137,6 +157,7 @@ impl<'a> Options<'a> {
query_context,
no_lazy,
custom_name,
ios,
})
}
}
Expand Down Expand Up @@ -262,6 +283,13 @@ pub fn from_command_line() -> ArgMatches {
.takes_value(true)
.display_order(8)
)
.arg(
Arg::new(OPTION_IOS)
.long(OPTION_IOS)
.help("Specify that the database is from an iOS backup\nUsing this option requires a custom path to the iPhone backup directory")
.display_order(9)
.requires(OPTION_DB_PATH)
)
.get_matches();
matches
}
Expand Down
10 changes: 8 additions & 2 deletions imessage-exporter/src/app/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use rusqlite::Connection;

use crate::{
app::{
converter::Converter, error::RuntimeError, options::Options, sanitizers::sanitize_filename,
converter::Converter, error::RuntimeError, options::{Options, DEFAULT_IOS_CHATDB_PATH}, sanitizers::sanitize_filename,
},
Exporter, HTML, TXT,
};
Expand Down Expand Up @@ -166,7 +166,12 @@ impl<'a> Config<'a> {
/// let app = Config::new(options).unwrap();
/// ```
pub fn new(options: Options) -> Result<Config, RuntimeError> {
let conn = get_connection(&options.db_path).map_err(RuntimeError::DatabaseError)?;
let conn = if options.ios {
ReagentX marked this conversation as resolved.
Show resolved Hide resolved
let ios_db_path = options.db_path.join(DEFAULT_IOS_CHATDB_PATH);
get_connection(&ios_db_path).map_err(RuntimeError::DatabaseError)?
} else {
get_connection(&options.db_path).map_err(RuntimeError::DatabaseError)?
};
eprintln!("Building cache...");
eprintln!("[1/4] Caching chats...");
let chatrooms = Chat::cache(&conn).map_err(RuntimeError::DatabaseError)?;
Expand Down Expand Up @@ -296,6 +301,7 @@ mod test {
query_context: QueryContext::default(),
no_lazy: false,
custom_name: None,
ios: false,
}
}

Expand Down
21 changes: 15 additions & 6 deletions imessage-exporter/src/exporters/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::{
path::{Path, PathBuf},
};

use sha1::{Digest, Sha1};

use crate::{
app::{
converter::heic_to_jpeg, error::RuntimeError, progress::build_progress_bar_export,
Expand Down Expand Up @@ -399,12 +401,18 @@ impl<'a> Writer<'a> for HTML<'a> {
match attachment.path() {
Some(path) => {
if let Some(path_str) = path.as_os_str().to_str() {
// Resolve the attachment path if necessary
let resolved_attachment_path = if path.starts_with("~") {
path_str.replace('~', &home())
} else {
path_str.to_owned()
};
let resolved_attachment_path =
// Resolve the relative attachment path to absolute for macOS
if path.starts_with("~") & !self.config.options.ios {
path_str.replace('~', &home())
// Resolve the attachment path for iOS backup
} else if self.config.options.ios {
let salt = "MediaDomain-";
let hash = format!("{:x}", Sha1::digest(format!("{}{}", salt, &path_str[2..]).as_bytes()));
ReagentX marked this conversation as resolved.
Show resolved Hide resolved
format!("{}/{}/{}", self.config.options.db_path.display(), &hash[0..2], &hash)
ReagentX marked this conversation as resolved.
Show resolved Hide resolved
} else {
path_str.to_owned()
};

// Perform optional copy + convert
if !self.config.options.no_copy {
Expand Down Expand Up @@ -1186,6 +1194,7 @@ mod tests {
query_context: QueryContext::default(),
no_lazy: false,
custom_name: None,
ios: false,
}
}

Expand Down
25 changes: 23 additions & 2 deletions imessage-exporter/src/exporters/txt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::{
path::{Path, PathBuf},
};

use sha1::{Digest, Sha1};

use crate::{
app::{error::RuntimeError, progress::build_progress_bar_export, runtime::Config},
exporters::exporter::{BalloonFormatter, Exporter, Writer},
Expand Down Expand Up @@ -276,9 +278,27 @@ impl<'a> Writer<'a> for TXT<'a> {
_: &Message,
) -> Result<String, &'a str> {
match &attachment.filename {
Some(filename) => Ok(filename.to_owned()),
Some(filename) => Ok(
if self.config.options.ios {
// iOS rehash the filename to a path in the backup
let salt = "MediaDomain-";
let hash = format!("{:x}", Sha1::digest(
format!("{}{}", salt, &filename[2..]).as_bytes()
ReagentX marked this conversation as resolved.
Show resolved Hide resolved
));
// attempting to escape spaces in the path for easier copy-paste
let db_path_os_string = self.config.options.db_path.to_str()
.map(|s| s.replace(" ", r#"\ "#))
.unwrap_or_else(|| self.config.options.db_path.display().to_string());
// <db_path>/<hash[0..2]>/<hash>/<filename> > <filename> allows for copy-paste conversion
format!("{}/{}/{} > {}", db_path_os_string, &hash[0..2], hash,
ReagentX marked this conversation as resolved.
Show resolved Hide resolved
filename.rsplit_once("/").unwrap().1)
Copy link
Owner

@ReagentX ReagentX Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use unwrap() in runtime code; I try to reserve it for tests only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, would this be better?

format!("{}/{}/{} > {}", db_path_os_string, &hash[0..2], hash,
        filename.rsplit_once("/").map(|(_, x)| x).unwrap_or_else(|| "Could not get filename")

or we could reflow the logic so it goes ios, macos, error in the match

match &attachment.filename {
  Some(filename) if self.config.options.ios => {
      // iOS rehash the filename to a path in the backup
      let salt = "MediaDomain-";
      let hash = format!("{:x}", Sha1::digest(
          format!("{}{}", salt, &filename[2..]).as_bytes()
      ));
      // attempting to escape spaces in the path for easier copy-paste
      let db_path_os_string = self.config.options.db_path.to_str()
          .map(|s| s.replace(" ", r#"\ "#))
          .unwrap_or_else(|| self.config.options.db_path.display().to_string());
      // <db_path>/<hash[0..2]>/<hash>/<filename> > <filename> allows for copy-paste conversion
      Ok(format!("{}/{}/{} > {}", db_path_os_string, &hash[0..2], hash,
                 filename.rsplit_once("/").map(|(_, x)| x).unwrap_or_else(|| "Could not get filename")))
  }
  // macOS uses the filename as the path
  Some(filename) => Ok(filename.to_string()), 
  None => Err(attachment.filename()),
}

and we could always swap out the "Could not get a filename" with just a plain old error like so

match filename.rsplit_once("/") {
    Some((_, filename)) => Ok(format!("{}/{}/{} > {}", db_path_os_string, &hash[0..2], hash, filename)),
    None => Err(attachment.filename()),
}

or returning it "macOS" style, which is the path from the internal iPhone file system

match filename.rsplit_once("/") {
    Some((_, filename)) => Ok(format!("{}/{}/{} > {}", db_path_os_string, &hash[0..2], hash, filename)),
    None => Ok(filename.to_string()), 
}

again I'm not entirely sure how you want the program to handle it or what syntactic style you prefer

Copy link
Owner

@ReagentX ReagentX Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think the logic for the iOS parsing should be split out somewhere so that it can be tested separately.

  2. I think this whole block will be a lot cleaner once we use an enum instead of a bool here, so I will wait to review it until that changes.

} else {
// macOS uses the filename as the path
filename.to_string()
}
),
// Filepath missing!
None => Err(attachment.filename()),
None => Err(attachment.filename())
}
}

Expand Down Expand Up @@ -729,6 +749,7 @@ mod tests {
query_context: QueryContext::default(),
no_lazy: false,
custom_name: None,
ios: false,
}
}

Expand Down