Allow missing fields

This commit is contained in:
2026-01-21 23:18:06 +01:00
parent 6c61aed7a1
commit 6802308239
2 changed files with 135 additions and 62 deletions

View File

@@ -24,16 +24,16 @@ impl Database {
timestamp TEXT NOT NULL, timestamp TEXT NOT NULL,
app TEXT NOT NULL, app TEXT NOT NULL,
version TEXT NOT NULL, version TEXT NOT NULL,
offline_login_usage INTEGER NOT NULL, offline_login_usage INTEGER,
is_password_autofill_enabled INTEGER NOT NULL, is_password_autofill_enabled INTEGER,
camera_roll_usage INTEGER NOT NULL, camera_roll_usage INTEGER,
os TEXT NOT NULL, os TEXT,
app_name TEXT NOT NULL, app_name TEXT,
touch_id INTEGER NOT NULL, touch_id INTEGER,
is_offline_login_enabled INTEGER NOT NULL, is_offline_login_enabled INTEGER,
model TEXT NOT NULL, model TEXT,
device TEXT NOT NULL, device TEXT,
password_autofill_usage INTEGER NOT NULL password_autofill_usage INTEGER
); );
CREATE INDEX IF NOT EXISTS idx_session_id ON signature_entries(session_id); CREATE INDEX IF NOT EXISTS idx_session_id ON signature_entries(session_id);
@@ -66,12 +66,12 @@ impl Database {
entry.app, entry.app,
entry.version, entry.version,
entry.offline_login_usage, 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.camera_roll_usage,
entry.os, entry.os,
entry.app_name, entry.app_name,
entry.touch_id as i32, entry.touch_id.map(|b| b as i32),
entry.is_offline_login_enabled as i32, entry.is_offline_login_enabled.map(|b| b as i32),
entry.model, entry.model,
entry.device, entry.device,
entry.password_autofill_usage, entry.password_autofill_usage,

View File

@@ -4,22 +4,22 @@ use regex::Regex;
use std::sync::LazyLock; use std::sync::LazyLock;
/// Represents a parsed signature log entry /// Represents a parsed signature log entry
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct SignatureEntry { pub struct SignatureEntry {
pub session_id: String, pub session_id: String,
pub timestamp: NaiveDateTime, pub timestamp: NaiveDateTime,
pub app: String, pub app: String,
pub version: String, pub version: String,
pub offline_login_usage: i64, pub offline_login_usage: Option<i64>,
pub is_password_autofill_enabled: bool, pub is_password_autofill_enabled: Option<bool>,
pub camera_roll_usage: i64, pub camera_roll_usage: Option<i64>,
pub os: String, pub os: Option<String>,
pub app_name: String, pub app_name: Option<String>,
pub touch_id: bool, pub touch_id: Option<bool>,
pub is_offline_login_enabled: bool, pub is_offline_login_enabled: Option<bool>,
pub model: String, pub model: Option<String>,
pub device: String, pub device: Option<String>,
pub password_autofill_usage: i64, pub password_autofill_usage: Option<i64>,
} }
/// Trait for parsing different message types from logs. /// Trait for parsing different message types from logs.
@@ -93,16 +93,16 @@ impl SignatureParser {
timestamp, timestamp,
app, app,
version, version,
offline_login_usage: parse_number(&details, "offlineLoginUsage")?, offline_login_usage: parse_number(&details, "offlineLoginUsage"),
is_password_autofill_enabled: parse_bool(&details, "isPasswordAutofillEnabled")?, is_password_autofill_enabled: parse_bool(&details, "isPasswordAutofillEnabled"),
camera_roll_usage: parse_number(&details, "cameraRollUsage")?, camera_roll_usage: parse_number(&details, "cameraRollUsage"),
os: get_string(&details, "OS")?, os: get_string(&details, "OS"),
app_name: get_string(&details, "appName")?, app_name: get_string(&details, "appName"),
touch_id: parse_bool(&details, "touchID")?, touch_id: parse_bool(&details, "touchID"),
is_offline_login_enabled: parse_bool(&details, "isOfflineLoginEnabled")?, is_offline_login_enabled: parse_bool(&details, "isOfflineLoginEnabled"),
model: get_string(&details, "model")?, model: get_string(&details, "model"),
device: get_string(&details, "device")?, device: get_string(&details, "device"),
password_autofill_usage: parse_number(&details, "passwordAutofillUsage")?, password_autofill_usage: parse_number(&details, "passwordAutofillUsage"),
}; };
Ok(ParsedMessage::Signature(entry)) Ok(ParsedMessage::Signature(entry))
@@ -168,28 +168,22 @@ fn parse_details(details: &str) -> Result<std::collections::HashMap<String, Stri
Ok(map) Ok(map)
} }
fn parse_number(map: &std::collections::HashMap<String, String>, key: &str) -> Result<i64> { fn parse_number(map: &std::collections::HashMap<String, String>, key: &str) -> Option<i64> {
map.get(key) map.get(key).and_then(|v| v.parse().ok())
.ok_or_else(|| anyhow!("Missing key: {}", key))?
.parse()
.map_err(|e| anyhow!("Invalid number for {}: {}", key, e))
} }
fn parse_bool(map: &std::collections::HashMap<String, String>, key: &str) -> Result<bool> { fn parse_bool(map: &std::collections::HashMap<String, String>, key: &str) -> Option<bool> {
let value = map map.get(key).and_then(|value| {
.get(key) match value.to_lowercase().as_str() {
.ok_or_else(|| anyhow!("Missing key: {}", key))?; "yes" | "true" | "1" => Some(true),
match value.to_lowercase().as_str() { "no" | "false" | "0" => Some(false),
"yes" | "true" | "1" => Ok(true), _ => None,
"no" | "false" | "0" => Ok(false), }
_ => Err(anyhow!("Invalid boolean for {}: {}", key, value)), })
}
} }
fn get_string(map: &std::collections::HashMap<String, String>, key: &str) -> Result<String> { fn get_string(map: &std::collections::HashMap<String, String>, key: &str) -> Option<String> {
map.get(key) map.get(key).map(|s| s.to_string())
.ok_or_else(|| anyhow!("Missing key: {}", key))
.map(|s| s.to_string())
} }
/// Registry of all available message parsers /// Registry of all available message parsers
@@ -244,16 +238,16 @@ mod tests {
assert_eq!(entry.session_id, "test-session-123"); assert_eq!(entry.session_id, "test-session-123");
assert_eq!(entry.app, "XAMARIN_APP"); assert_eq!(entry.app, "XAMARIN_APP");
assert_eq!(entry.version, "5.23.0"); assert_eq!(entry.version, "5.23.0");
assert_eq!(entry.offline_login_usage, 0); assert_eq!(entry.offline_login_usage, Some(0));
assert!(!entry.is_password_autofill_enabled); assert_eq!(entry.is_password_autofill_enabled, Some(false));
assert_eq!(entry.camera_roll_usage, 0); assert_eq!(entry.camera_roll_usage, Some(0));
assert_eq!(entry.os, "26.2.0"); assert_eq!(entry.os, Some("26.2.0".to_string()));
assert_eq!(entry.app_name, "App"); assert_eq!(entry.app_name, Some("App".to_string()));
assert!(!entry.touch_id); assert_eq!(entry.touch_id, Some(false));
assert!(entry.is_offline_login_enabled); assert_eq!(entry.is_offline_login_enabled, Some(true));
assert_eq!(entry.model, "iPhone18,1"); assert_eq!(entry.model, Some("iPhone18,1".to_string()));
assert_eq!(entry.device, "iOS, Apple"); assert_eq!(entry.device, Some("iOS, Apple".to_string()));
assert_eq!(entry.password_autofill_usage, 0); assert_eq!(entry.password_autofill_usage, Some(0));
} }
} }
} }
@@ -264,4 +258,83 @@ mod tests {
let registry = ParserRegistry::new(); let registry = ParserRegistry::new();
assert!(registry.parse(line).is_none()); 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()));
}
}
}
} }