1#![forbid(unsafe_code)]
34#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
35
36mod error;
37mod record;
38mod timeline;
39
40pub use error::WireError;
41pub use record::{
42 interpret_records, ObjectStoreSummary, PayloadState, WireRecord, WireRecordKind, WireStore,
43};
44pub use timeline::{timeline, TimelineEntry};
45
46use forensicnomicon_core::messenger_desktop::{self, MessengerSpec, StoreRole};
47use std::path::{Path, PathBuf};
48
49#[must_use]
51pub fn wire_spec() -> Option<&'static MessengerSpec> {
52 messenger_desktop::spec("Wire")
53}
54
55pub fn read_store(dir: &Path) -> Result<WireStore, WireError> {
61 let records = chromium_storage_indexeddb::read_dir(dir).map_err(|e| WireError::Read {
62 path: dir.display().to_string(),
63 detail: e.to_string(),
64 })?;
65 Ok(interpret_records(&records))
66}
67
68pub fn read_profile(base: &Path) -> Result<WireStore, WireError> {
76 let spec = wire_spec().ok_or_else(|| WireError::StoreNotFound {
79 base: base.display().to_string(), relative: "<Wire spec missing>".to_string(), })?; let relative = spec
83 .store(StoreRole::Messages)
84 .map_or("IndexedDB", |s| s.relative_path);
85 let dir: PathBuf = base.join(relative);
86 if !dir.is_dir() {
87 return Err(WireError::StoreNotFound {
88 base: base.display().to_string(),
89 relative: relative.to_string(),
90 });
91 }
92 read_store(&dir)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn wire_spec_resolves_from_knowledge_leaf() {
101 let spec = wire_spec().expect("Wire spec present in forensicnomicon-core");
102 assert_eq!(spec.app, "Wire");
103 let msgs = spec.store(StoreRole::Messages).expect("messages store");
105 assert!(msgs.relative_path.contains(".indexeddb.leveldb"));
106 }
107
108 #[test]
109 fn read_profile_fails_loud_when_store_absent() {
110 let tmp = std::env::temp_dir().join("wire-desktop-core-nonexistent-profile-xyz");
111 let err = read_profile(&tmp).expect_err("absent store must fail loud");
112 assert!(matches!(err, WireError::StoreNotFound { .. }));
113 assert!(err.to_string().contains(".indexeddb.leveldb"));
114 }
115
116 #[test]
117 fn read_profile_reads_a_store_at_the_spec_relative_path() {
118 let spec = wire_spec().expect("spec");
121 let relative = spec
122 .store(StoreRole::Messages)
123 .expect("messages")
124 .relative_path;
125
126 let base = std::env::temp_dir().join(format!("wire-profile-{}", std::process::id()));
127 let dest = base.join(relative);
128 std::fs::create_dir_all(&dest).expect("mkdir profile store");
129 let src = std::path::PathBuf::from(concat!(
130 env!("CARGO_MANIFEST_DIR"),
131 "/../tests/data/wire-indexeddb/http_127.0.0.1_8731.indexeddb.leveldb"
132 ));
133 for entry in std::fs::read_dir(&src).expect("read fixture dir") {
134 let entry = entry.expect("entry");
135 std::fs::copy(entry.path(), dest.join(entry.file_name())).expect("copy fixture file");
136 }
137
138 let store = read_profile(&base).expect("read_profile happy path");
139 assert!(store
140 .records
141 .iter()
142 .any(|r| r.text.as_deref() == Some("meet at 9")));
143
144 std::fs::remove_dir_all(&base).ok();
145 }
146
147 #[test]
148 fn read_store_surfaces_reader_errors() {
149 let missing = std::env::temp_dir().join("wire-desktop-core-no-such-store-dir");
150 let err = read_store(&missing).expect_err("missing dir must error");
151 assert!(matches!(err, WireError::Read { .. }));
152 assert!(err.to_string().contains("no-such-store-dir"));
153 }
154}