#![forbid(unsafe_code)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
mod error;
mod record;
mod timeline;
pub use error::WireError;
pub use record::{
interpret_records, ObjectStoreSummary, PayloadState, WireRecord, WireRecordKind, WireStore,
};
pub use timeline::{timeline, TimelineEntry};
use forensicnomicon_core::messenger_desktop::{self, MessengerSpec, StoreRole};
use std::path::{Path, PathBuf};
#[must_use]
pub fn wire_spec() -> Option<&'static MessengerSpec> {
messenger_desktop::spec("Wire")
}
pub fn read_store(dir: &Path) -> Result<WireStore, WireError> {
let records = chromium_storage_indexeddb::read_dir(dir).map_err(|e| WireError::Read {
path: dir.display().to_string(),
detail: e.to_string(),
})?;
Ok(interpret_records(&records))
}
pub fn read_profile(base: &Path) -> Result<WireStore, WireError> {
let spec = wire_spec().ok_or_else(|| WireError::StoreNotFound {
base: base.display().to_string(), relative: "<Wire spec missing>".to_string(), })?; let relative = spec
.store(StoreRole::Messages)
.map_or("IndexedDB", |s| s.relative_path);
let dir: PathBuf = base.join(relative);
if !dir.is_dir() {
return Err(WireError::StoreNotFound {
base: base.display().to_string(),
relative: relative.to_string(),
});
}
read_store(&dir)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_spec_resolves_from_knowledge_leaf() {
let spec = wire_spec().expect("Wire spec present in forensicnomicon-core");
assert_eq!(spec.app, "Wire");
let msgs = spec.store(StoreRole::Messages).expect("messages store");
assert!(msgs.relative_path.contains(".indexeddb.leveldb"));
}
#[test]
fn read_profile_fails_loud_when_store_absent() {
let tmp = std::env::temp_dir().join("wire-desktop-core-nonexistent-profile-xyz");
let err = read_profile(&tmp).expect_err("absent store must fail loud");
assert!(matches!(err, WireError::StoreNotFound { .. }));
assert!(err.to_string().contains(".indexeddb.leveldb"));
}
#[test]
fn read_profile_reads_a_store_at_the_spec_relative_path() {
let spec = wire_spec().expect("spec");
let relative = spec
.store(StoreRole::Messages)
.expect("messages")
.relative_path;
let base = std::env::temp_dir().join(format!("wire-profile-{}", std::process::id()));
let dest = base.join(relative);
std::fs::create_dir_all(&dest).expect("mkdir profile store");
let src = std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../tests/data/wire-indexeddb/http_127.0.0.1_8731.indexeddb.leveldb"
));
for entry in std::fs::read_dir(&src).expect("read fixture dir") {
let entry = entry.expect("entry");
std::fs::copy(entry.path(), dest.join(entry.file_name())).expect("copy fixture file");
}
let store = read_profile(&base).expect("read_profile happy path");
assert!(store
.records
.iter()
.any(|r| r.text.as_deref() == Some("meet at 9")));
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn read_store_surfaces_reader_errors() {
let missing = std::env::temp_dir().join("wire-desktop-core-no-such-store-dir");
let err = read_store(&missing).expect_err("missing dir must error");
assert!(matches!(err, WireError::Read { .. }));
assert!(err.to_string().contains("no-such-store-dir"));
}
}