1use chrono::{DateTime, FixedOffset, Utc};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
6pub enum EvidenceSource {
7 Live,
8 WalPending,
9 WalHistoric,
10 Freelist,
11 FtsOnly,
12 CarvedUnalloc { confidence_pct: u8 },
13 CarvedDb,
14}
15
16impl fmt::Display for EvidenceSource {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Live => write!(f, "LIVE"),
20 Self::WalPending => write!(f, "WAL-PENDING"),
21 Self::WalHistoric => write!(f, "WAL-HISTORIC"),
22 Self::Freelist => write!(f, "FREELIST"),
23 Self::FtsOnly => write!(f, "FTS-ONLY"),
24 Self::CarvedUnalloc { confidence_pct } => {
25 write!(f, "CARVED-UNALLOC {confidence_pct}%")
26 }
27 Self::CarvedDb => write!(f, "CARVED-DB"),
28 }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct ForensicTimestamp {
34 pub utc: DateTime<Utc>,
35 pub local_offset_seconds: i32,
36}
37
38impl ForensicTimestamp {
39 pub fn from_millis(ms: i64, local_offset_seconds: i32) -> Self {
40 use chrono::TimeZone;
41 let utc = Utc
42 .timestamp_millis_opt(ms)
43 .single()
44 .unwrap_or(DateTime::<Utc>::UNIX_EPOCH);
45 Self {
46 utc,
47 local_offset_seconds,
48 }
49 }
50
51 pub fn utc_str(&self) -> String {
52 self.utc.format("%Y-%m-%d %H:%M:%S UTC").to_string()
53 }
54
55 pub fn local_str(&self) -> String {
56 let offset = FixedOffset::east_opt(self.local_offset_seconds)
57 .unwrap_or(FixedOffset::east_opt(0).unwrap());
58 let local = self.utc.with_timezone(&offset);
59 let total_secs = self.local_offset_seconds;
60 let sign = if total_secs >= 0 { '+' } else { '-' };
61 let abs_secs = total_secs.unsigned_abs();
62 let hours = abs_secs / 3600;
63 let mins = (abs_secs % 3600) / 60;
64 format!(
65 "{} | {} {}{:02}:{:02}",
66 self.utc.format("%Y-%m-%d %H:%M:%S UTC"),
67 local.format("%Y-%m-%d %H:%M:%S"),
68 sign,
69 hours,
70 mins
71 )
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct MediaRef {
77 pub file_path: String,
78 pub mime_type: String,
79 pub file_size: u64,
80 pub extracted_name: Option<String>,
81 pub thumbnail_b64: Option<String>,
82 pub duration_secs: Option<u32>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub struct Reaction {
87 pub emoji: String,
88 pub reactor_jid: String,
89 pub timestamp: ForensicTimestamp,
90 pub source: EvidenceSource,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
94pub struct CallRecord {
95 pub call_id: i64,
96 pub participants: Vec<String>,
97 pub from_me: bool,
98 pub video: bool,
99 pub group_call: bool,
100 pub duration_secs: u32,
101 pub timestamp: ForensicTimestamp,
102 pub source: EvidenceSource,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106pub enum MessageContent {
107 Text(String),
108 Media(MediaRef),
109 Location {
110 lat: f64,
111 lon: f64,
112 name: Option<String>,
113 },
114 VCard(String),
115 Deleted,
116 System(String),
117 Unknown(i32),
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct Message {
122 pub id: i64,
123 pub chat_id: i64,
124 pub sender_jid: Option<String>,
125 pub from_me: bool,
126 pub timestamp: ForensicTimestamp,
127 pub content: MessageContent,
128 pub reactions: Vec<Reaction>,
129 pub quoted_message: Option<Box<Message>>,
130 pub source: EvidenceSource,
131 pub row_offset: u64,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct Contact {
136 pub jid: String,
137 pub display_name: Option<String>,
138 pub phone_number: Option<String>,
139 pub source: EvidenceSource,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct Chat {
144 pub id: i64,
145 pub jid: String,
146 pub name: Option<String>,
147 pub is_group: bool,
148 pub messages: Vec<Message>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub enum WalDeltaStatus {
153 AddedInWal,
154 DeletedInWal,
155 ModifiedInWal,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct WalDelta {
160 pub table: String,
161 pub row_id: i64,
162 pub status: WalDeltaStatus,
163}
164
165#[derive(Debug, Default, Serialize, Deserialize)]
166pub struct ExtractionResult {
167 pub chats: Vec<Chat>,
168 pub contacts: Vec<Contact>,
169 pub calls: Vec<CallRecord>,
170 pub wal_deltas: Vec<WalDelta>,
171 pub timezone_offset_seconds: Option<i32>,
172 pub schema_version: u32,
173}