Allow missing fields
This commit is contained in:
26
src/db.rs
26
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,
|
||||
|
||||
171
src/parser.rs
171
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<i64>,
|
||||
pub is_password_autofill_enabled: Option<bool>,
|
||||
pub camera_roll_usage: Option<i64>,
|
||||
pub os: Option<String>,
|
||||
pub app_name: Option<String>,
|
||||
pub touch_id: Option<bool>,
|
||||
pub is_offline_login_enabled: Option<bool>,
|
||||
pub model: Option<String>,
|
||||
pub device: Option<String>,
|
||||
pub password_autofill_usage: Option<i64>,
|
||||
}
|
||||
|
||||
/// 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<std::collections::HashMap<String, Stri
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn parse_number(map: &std::collections::HashMap<String, String>, key: &str) -> Result<i64> {
|
||||
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<String, String>, key: &str) -> Option<i64> {
|
||||
map.get(key).and_then(|v| v.parse().ok())
|
||||
}
|
||||
|
||||
fn parse_bool(map: &std::collections::HashMap<String, String>, key: &str) -> Result<bool> {
|
||||
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<String, String>, key: &str) -> Option<bool> {
|
||||
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<String, String>, key: &str) -> Result<String> {
|
||||
map.get(key)
|
||||
.ok_or_else(|| anyhow!("Missing key: {}", key))
|
||||
.map(|s| s.to_string())
|
||||
fn get_string(map: &std::collections::HashMap<String, String>, key: &str) -> Option<String> {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user