diff --git a/src/db.rs b/src/db.rs index f65969e..17896c2 100644 --- a/src/db.rs +++ b/src/db.rs @@ -55,9 +55,10 @@ impl Database { ); -- Main table with normalized foreign keys and integer timestamp + -- timestamp_ms stores milliseconds since epoch for uniqueness CREATE TABLE IF NOT EXISTS signature_entries ( session_id TEXT NOT NULL, - timestamp INTEGER NOT NULL, + timestamp_ms INTEGER NOT NULL, app_id INTEGER NOT NULL REFERENCES apps(id), version_id INTEGER NOT NULL REFERENCES versions(id), offline_login_usage INTEGER, @@ -70,7 +71,7 @@ impl Database { model_id INTEGER REFERENCES models(id), device_id INTEGER REFERENCES devices(id), password_autofill_usage INTEGER, - PRIMARY KEY (session_id, timestamp) + PRIMARY KEY (session_id, timestamp_ms) ) WITHOUT ROWID; CREATE INDEX IF NOT EXISTS idx_session_id ON signature_entries(session_id); @@ -96,7 +97,7 @@ impl Database { let mut insert_stmt = tx.prepare_cached( r#" INSERT INTO signature_entries ( - session_id, timestamp, app_id, version_id, + session_id, timestamp_ms, app_id, version_id, offline_login_usage, is_password_autofill_enabled, camera_roll_usage, os_id, app_name_id, touch_id, is_offline_login_enabled, model_id, device_id, password_autofill_usage @@ -114,7 +115,7 @@ impl Database { insert_stmt.execute(params![ entry.session_id, - entry.timestamp.and_utc().timestamp(), + entry.timestamp_ms, app_id, version_id, entry.offline_login_usage, diff --git a/src/parser.rs b/src/parser.rs index c447fae..b663776 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,8 @@ use std::sync::LazyLock; #[derive(Debug, Clone, PartialEq)] pub struct SignatureEntry { pub session_id: String, - pub timestamp: NaiveDateTime, + /// Timestamp as milliseconds since Unix epoch (UTC) + pub timestamp_ms: i64, pub app: String, pub version: String, pub offline_login_usage: Option, @@ -39,7 +40,7 @@ pub enum ParsedMessage { static SESSION_ID_RE: LazyLock = LazyLock::new(|| Regex::new(r"sessionId=([^,\s]+)").unwrap()); static DATETIME_RE: LazyLock = - LazyLock::new(|| Regex::new(r#"dt="(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"#).unwrap()); + LazyLock::new(|| Regex::new(r#"dt="(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})(?:,(\d{3}))?"#).unwrap()); static CORRELATION_ID_RE: LazyLock = LazyLock::new(|| Regex::new(r"correlationId=([^,\s]+)").unwrap()); static SIGNATURE_RE: LazyLock = @@ -84,15 +85,8 @@ impl SignatureParser { .map(|m| m.as_str().to_string()) .ok_or_else(|| anyhow!("Missing sessionId"))?; - // Extract timestamp - let datetime_str = DATETIME_RE - .captures(line) - .and_then(|c| c.get(1)) - .map(|m| m.as_str()) - .ok_or_else(|| anyhow!("Missing datetime"))?; - - let timestamp = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S") - .map_err(|e| anyhow!("Invalid datetime format: {}", e))?; + // Extract timestamp as milliseconds + let timestamp_ms = extract_timestamp_ms(line)?; // Extract signature details let caps = SIGNATURE_RE @@ -109,7 +103,7 @@ impl SignatureParser { let entry = SignatureEntry { session_id, - timestamp, + timestamp_ms, app, version, offline_login_usage: parse_number(&details, "offlineLoginUsage"), @@ -143,7 +137,7 @@ impl MessageParser for MobileClientIosParser { impl MobileClientIosParser { fn parse_mobile_ios_line(&self, line: &str) -> Result { - let timestamp = extract_timestamp(line)?; + let timestamp_ms = extract_timestamp_ms(line)?; let session_id = extract_correlation_id(line)?; let caps = MOBILE_IOS_RE @@ -158,7 +152,7 @@ impl MobileClientIosParser { let entry = SignatureEntry { session_id, - timestamp, + timestamp_ms, app, version, offline_login_usage: None, @@ -192,7 +186,7 @@ impl MessageParser for MobileClientAndroidParser { impl MobileClientAndroidParser { fn parse_mobile_android_line(&self, line: &str) -> Result { - let timestamp = extract_timestamp(line)?; + let timestamp_ms = extract_timestamp_ms(line)?; let session_id = extract_correlation_id(line)?; let caps = MOBILE_ANDROID_RE @@ -207,7 +201,7 @@ impl MobileClientAndroidParser { let entry = SignatureEntry { session_id, - timestamp, + timestamp_ms, app, version, offline_login_usage: None, @@ -226,16 +220,22 @@ impl MobileClientAndroidParser { } } -/// Extract timestamp from log line -fn extract_timestamp(line: &str) -> Result { - let datetime_str = DATETIME_RE +/// Extract timestamp from log line as milliseconds since Unix epoch +fn extract_timestamp_ms(line: &str) -> Result { + let caps = DATETIME_RE .captures(line) - .and_then(|c| c.get(1)) - .map(|m| m.as_str()) .ok_or_else(|| anyhow!("Missing datetime"))?; - NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S") - .map_err(|e| anyhow!("Invalid datetime format: {}", e)) + let datetime_str = caps.get(1).map(|m| m.as_str()).unwrap(); + let millis: i64 = caps + .get(2) + .map(|m| m.as_str().parse().unwrap_or(0)) + .unwrap_or(0); + + let dt = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S") + .map_err(|e| anyhow!("Invalid datetime format: {}", e))?; + + Ok(dt.and_utc().timestamp_millis() + millis) } /// Extract correlation ID as session ID for mobile client logs