From 6802308239e31787beb2ffe0c955eef9c1daa29f Mon Sep 17 00:00:00 2001 From: Alexandr Mansurov Date: Wed, 21 Jan 2026 23:18:06 +0100 Subject: [PATCH] Allow missing fields --- src/db.rs | 26 ++++---- src/parser.rs | 171 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 135 insertions(+), 62 deletions(-) diff --git a/src/db.rs b/src/db.rs index 09d6790..804558f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -24,16 +24,16 @@ impl Database { timestamp TEXT NOT NULL, app TEXT NOT NULL, version TEXT NOT NULL, - offline_login_usage INTEGER NOT NULL, - is_password_autofill_enabled INTEGER NOT NULL, - camera_roll_usage INTEGER NOT NULL, - os TEXT NOT NULL, - app_name TEXT NOT NULL, - touch_id INTEGER NOT NULL, - is_offline_login_enabled INTEGER NOT NULL, - model TEXT NOT NULL, - device TEXT NOT NULL, - password_autofill_usage INTEGER NOT NULL + offline_login_usage INTEGER, + is_password_autofill_enabled INTEGER, + camera_roll_usage INTEGER, + os TEXT, + app_name TEXT, + touch_id INTEGER, + is_offline_login_enabled INTEGER, + model TEXT, + device TEXT, + password_autofill_usage INTEGER ); CREATE INDEX IF NOT EXISTS idx_session_id ON signature_entries(session_id); @@ -66,12 +66,12 @@ impl Database { entry.app, entry.version, entry.offline_login_usage, - entry.is_password_autofill_enabled as i32, + entry.is_password_autofill_enabled.map(|b| b as i32), entry.camera_roll_usage, entry.os, entry.app_name, - entry.touch_id as i32, - entry.is_offline_login_enabled as i32, + entry.touch_id.map(|b| b as i32), + entry.is_offline_login_enabled.map(|b| b as i32), entry.model, entry.device, entry.password_autofill_usage, diff --git a/src/parser.rs b/src/parser.rs index ad15d01..1651f90 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,22 +4,22 @@ use regex::Regex; use std::sync::LazyLock; /// Represents a parsed signature log entry -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct SignatureEntry { pub session_id: String, pub timestamp: NaiveDateTime, pub app: String, pub version: String, - pub offline_login_usage: i64, - pub is_password_autofill_enabled: bool, - pub camera_roll_usage: i64, - pub os: String, - pub app_name: String, - pub touch_id: bool, - pub is_offline_login_enabled: bool, - pub model: String, - pub device: String, - pub password_autofill_usage: i64, + pub offline_login_usage: Option, + pub is_password_autofill_enabled: Option, + pub camera_roll_usage: Option, + pub os: Option, + pub app_name: Option, + pub touch_id: Option, + pub is_offline_login_enabled: Option, + pub model: Option, + pub device: Option, + pub password_autofill_usage: Option, } /// Trait for parsing different message types from logs. @@ -93,16 +93,16 @@ impl SignatureParser { timestamp, app, version, - offline_login_usage: parse_number(&details, "offlineLoginUsage")?, - is_password_autofill_enabled: parse_bool(&details, "isPasswordAutofillEnabled")?, - camera_roll_usage: parse_number(&details, "cameraRollUsage")?, - os: get_string(&details, "OS")?, - app_name: get_string(&details, "appName")?, - touch_id: parse_bool(&details, "touchID")?, - is_offline_login_enabled: parse_bool(&details, "isOfflineLoginEnabled")?, - model: get_string(&details, "model")?, - device: get_string(&details, "device")?, - password_autofill_usage: parse_number(&details, "passwordAutofillUsage")?, + offline_login_usage: parse_number(&details, "offlineLoginUsage"), + is_password_autofill_enabled: parse_bool(&details, "isPasswordAutofillEnabled"), + camera_roll_usage: parse_number(&details, "cameraRollUsage"), + os: get_string(&details, "OS"), + app_name: get_string(&details, "appName"), + touch_id: parse_bool(&details, "touchID"), + is_offline_login_enabled: parse_bool(&details, "isOfflineLoginEnabled"), + model: get_string(&details, "model"), + device: get_string(&details, "device"), + password_autofill_usage: parse_number(&details, "passwordAutofillUsage"), }; Ok(ParsedMessage::Signature(entry)) @@ -168,28 +168,22 @@ fn parse_details(details: &str) -> Result, key: &str) -> Result { - map.get(key) - .ok_or_else(|| anyhow!("Missing key: {}", key))? - .parse() - .map_err(|e| anyhow!("Invalid number for {}: {}", key, e)) +fn parse_number(map: &std::collections::HashMap, key: &str) -> Option { + map.get(key).and_then(|v| v.parse().ok()) } -fn parse_bool(map: &std::collections::HashMap, key: &str) -> Result { - let value = map - .get(key) - .ok_or_else(|| anyhow!("Missing key: {}", key))?; - match value.to_lowercase().as_str() { - "yes" | "true" | "1" => Ok(true), - "no" | "false" | "0" => Ok(false), - _ => Err(anyhow!("Invalid boolean for {}: {}", key, value)), - } +fn parse_bool(map: &std::collections::HashMap, key: &str) -> Option { + map.get(key).and_then(|value| { + match value.to_lowercase().as_str() { + "yes" | "true" | "1" => Some(true), + "no" | "false" | "0" => Some(false), + _ => None, + } + }) } -fn get_string(map: &std::collections::HashMap, key: &str) -> Result { - map.get(key) - .ok_or_else(|| anyhow!("Missing key: {}", key)) - .map(|s| s.to_string()) +fn get_string(map: &std::collections::HashMap, key: &str) -> Option { + map.get(key).map(|s| s.to_string()) } /// Registry of all available message parsers @@ -244,16 +238,16 @@ mod tests { assert_eq!(entry.session_id, "test-session-123"); assert_eq!(entry.app, "XAMARIN_APP"); assert_eq!(entry.version, "5.23.0"); - assert_eq!(entry.offline_login_usage, 0); - assert!(!entry.is_password_autofill_enabled); - assert_eq!(entry.camera_roll_usage, 0); - assert_eq!(entry.os, "26.2.0"); - assert_eq!(entry.app_name, "App"); - assert!(!entry.touch_id); - assert!(entry.is_offline_login_enabled); - assert_eq!(entry.model, "iPhone18,1"); - assert_eq!(entry.device, "iOS, Apple"); - assert_eq!(entry.password_autofill_usage, 0); + assert_eq!(entry.offline_login_usage, Some(0)); + assert_eq!(entry.is_password_autofill_enabled, Some(false)); + assert_eq!(entry.camera_roll_usage, Some(0)); + assert_eq!(entry.os, Some("26.2.0".to_string())); + assert_eq!(entry.app_name, Some("App".to_string())); + assert_eq!(entry.touch_id, Some(false)); + assert_eq!(entry.is_offline_login_enabled, Some(true)); + assert_eq!(entry.model, Some("iPhone18,1".to_string())); + assert_eq!(entry.device, Some("iOS, Apple".to_string())); + assert_eq!(entry.password_autofill_usage, Some(0)); } } } @@ -264,4 +258,83 @@ mod tests { let registry = ParserRegistry::new(); assert!(registry.parse(line).is_none()); } + + #[test] + fn test_parse_signature_with_missing_offline_login_usage() { + // Line missing offlineLoginUsage field + let line = r#"Jan 21 00:00:06 tom013 m1s-kv dt="2026-01-21 00:00:06,154", sessionId=test-123, msg="signature:XAMARIN_APP/5.23.0/ details:isPasswordAutofillEnabled:yes,cameraRollUsage:1,OS:26.2.0,appName:App,touchID:yes,isOfflineLoginEnabled:no,model:iPhone15,3,device:iOS, Apple,passwordAutofillUsage:2 user-agent:test", ex=""#; + + let registry = ParserRegistry::new(); + let result = registry.parse(line).unwrap().unwrap(); + + match result { + ParsedMessage::Signature(entry) => { + assert_eq!(entry.session_id, "test-123"); + assert_eq!(entry.app, "XAMARIN_APP"); + assert_eq!(entry.version, "5.23.0"); + // Missing field should be None + assert_eq!(entry.offline_login_usage, None); + // Other fields should be present + assert_eq!(entry.is_password_autofill_enabled, Some(true)); + assert_eq!(entry.camera_roll_usage, Some(1)); + assert_eq!(entry.os, Some("26.2.0".to_string())); + assert_eq!(entry.app_name, Some("App".to_string())); + assert_eq!(entry.touch_id, Some(true)); + assert_eq!(entry.is_offline_login_enabled, Some(false)); + assert_eq!(entry.model, Some("iPhone15,3".to_string())); + assert_eq!(entry.device, Some("iOS, Apple".to_string())); + assert_eq!(entry.password_autofill_usage, Some(2)); + } + } + } + + #[test] + fn test_parse_signature_with_missing_password_autofill_usage() { + // Line missing passwordAutofillUsage (truncated log line scenario) + let line = r#"Jan 21 00:00:06 tom013 m1s-kv dt="2026-01-21 00:00:06,154", sessionId=test-456, msg="signature:XAMARIN_APP/5.23.0/ details:offlineLoginUsage:0,isPasswordAutofillEnabled:no,cameraRollUsage:0,OS:16.0.0,appName:App,touchID:yes,isOfflineLoginEnabled:yes,model:SM-S938B,device:Android", ex=""#; + + let registry = ParserRegistry::new(); + let result = registry.parse(line).unwrap().unwrap(); + + match result { + ParsedMessage::Signature(entry) => { + assert_eq!(entry.session_id, "test-456"); + assert_eq!(entry.app, "XAMARIN_APP"); + // passwordAutofillUsage missing + assert_eq!(entry.password_autofill_usage, None); + // Other fields present + assert_eq!(entry.offline_login_usage, Some(0)); + assert_eq!(entry.device, Some("Android".to_string())); + } + } + } + + #[test] + fn test_parse_signature_with_multiple_missing_fields() { + // Line missing multiple fields + let line = r#"Jan 21 00:00:06 tom013 m1s-kv dt="2026-01-21 00:00:06,154", sessionId=test-789, msg="signature:XAMARIN_APP/5.23.0/ details:OS:26.2.0,appName:App,model:iPhone18,1 user-agent:test", ex=""#; + + let registry = ParserRegistry::new(); + let result = registry.parse(line).unwrap().unwrap(); + + match result { + ParsedMessage::Signature(entry) => { + assert_eq!(entry.session_id, "test-789"); + assert_eq!(entry.app, "XAMARIN_APP"); + assert_eq!(entry.version, "5.23.0"); + // Missing fields should be None + assert_eq!(entry.offline_login_usage, None); + assert_eq!(entry.is_password_autofill_enabled, None); + assert_eq!(entry.camera_roll_usage, None); + assert_eq!(entry.touch_id, None); + assert_eq!(entry.is_offline_login_enabled, None); + assert_eq!(entry.device, None); + assert_eq!(entry.password_autofill_usage, None); + // Present fields should have values + assert_eq!(entry.os, Some("26.2.0".to_string())); + assert_eq!(entry.app_name, Some("App".to_string())); + assert_eq!(entry.model, Some("iPhone18,1".to_string())); + } + } + } }