use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use iwcore::backup::{check_db_version, get_db_version};
use iwcore::{IWField, IWItem, Wallet};
use tempfile::TempDir;
const TEST_PASSWORD: &str = "KuiperBelt30au";
fn testdata_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata")
}
fn setup_wallet(db_filename: &str) -> (Wallet, TempDir) {
let source = testdata_dir().join(db_filename);
assert!(source.exists(), "Test database not found: {:?}", source);
let temp_dir = TempDir::new().unwrap();
let dest = temp_dir.path().join("nswallet.dat");
fs::copy(&source, &dest).expect("Failed to copy test database");
let wallet = Wallet::open(temp_dir.path()).expect("Failed to open wallet");
(wallet, temp_dir)
}
#[test]
fn dump_database_contents() {
dump_main_database();
dump_version_only("nswallet_old.dat");
dump_version_only("nswallet_from_future.dat");
let td = testdata_dir();
for name in ["nswallet.dat.md", "nswallet_old.dat.md", "nswallet_from_future.dat.md"] {
let path = td.join(name);
if path.exists() {
fs::remove_file(&path).ok();
}
}
}
fn dump_main_database() {
let (mut wallet, _temp_dir) = setup_wallet("nswallet.dat");
wallet.unlock(TEST_PASSWORD).unwrap();
let properties = wallet.get_properties().unwrap();
let labels = wallet.get_labels().unwrap();
let items: Vec<_> = wallet.get_items().unwrap().to_vec();
let fields: Vec<_> = wallet.get_fields().unwrap().to_vec();
let deleted_items = wallet.get_deleted_items().unwrap();
let deleted_fields = wallet.get_deleted_fields().unwrap();
wallet.close();
let item_map: HashMap<&str, &IWItem> =
items.iter().map(|i| (i.item_id.as_str(), i)).collect();
let deleted_map: HashMap<&str, &IWItem> =
deleted_items.iter().map(|i| (i.item_id.as_str(), i)).collect();
let compute_path = |item: &IWItem| -> String {
let mut parts: Vec<String> = Vec::new();
let mut current_parent = item.parent_id.as_deref();
loop {
match current_parent {
None => break,
Some("__ROOT__") => break,
Some(pid) => {
if let Some(parent) = item_map.get(pid).or_else(|| deleted_map.get(pid)) {
if parent.is_root() {
break;
}
parts.push(parent.name.clone());
current_parent = parent.parent_id.as_deref();
} else {
parts.push(format!("?{}", pid));
break;
}
}
}
}
parts.reverse();
if parts.is_empty() {
"/".to_string()
} else {
format!("/{}", parts.join("/"))
}
};
let mut fields_by_item: HashMap<&str, Vec<&IWField>> =
HashMap::new();
for f in &fields {
fields_by_item.entry(f.item_id.as_str()).or_default().push(f);
}
for v in fields_by_item.values_mut() {
v.sort_by_key(|f| f.sort_weight);
}
let mut deleted_fields_by_item: HashMap<&str, Vec<&IWField>> =
HashMap::new();
for f in &deleted_fields {
deleted_fields_by_item
.entry(f.item_id.as_str())
.or_default()
.push(f);
}
for v in deleted_fields_by_item.values_mut() {
v.sort_by_key(|f| f.sort_weight);
}
let mut active_items: Vec<_> = items.iter().filter(|i| !i.is_root()).collect();
active_items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
let mut sorted_deleted: Vec<_> = deleted_items.iter().collect();
sorted_deleted.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
let mut md = String::new();
md.push_str("# nswallet.dat\n\n");
md.push_str("> Auto-generated by `cargo test dump_database_contents`\n\n");
md.push_str("## Properties\n\n");
md.push_str("| Property | Value |\n");
md.push_str("|----------|-------|\n");
md.push_str(&format!("| Version | {} |\n", properties.version));
md.push_str(&format!("| Language | {} |\n", properties.lang));
md.push_str(&format!(
"| Encryption Count | {} |\n",
properties.encryption_count
));
md.push_str(&format!("| Database ID | {} |\n", properties.database_id));
md.push_str(&format!(
"| Sync Timestamp | {} |\n",
properties
.sync_timestamp
.map_or("–".to_string(), |t| t.to_string())
));
md.push_str(&format!(
"| Update Timestamp | {} |\n",
properties
.update_timestamp
.map_or("–".to_string(), |t| t.to_string())
));
md.push('\n');
md.push_str("## Labels\n\n");
md.push_str("| Type | Name | Value Type | Icon | System |\n");
md.push_str("|------|------|------------|------|--------|\n");
for label in &labels {
md.push_str(&format!(
"| {} | {} | {} | {} | {} |\n",
label.field_type,
label.name,
label.value_type,
label.icon,
if label.system { "yes" } else { "no" }
));
}
md.push('\n');
md.push_str("## Active Records\n\n");
for item in &active_items {
let path = compute_path(item);
md.push_str(&format!(
"### {} (id: {}, icon: {}, folder: {})\n\n",
item.name,
item.item_id,
item.icon,
if item.folder { "yes" } else { "no" }
));
md.push_str(&format!("Path: `{}`\n\n", path));
if let Some(item_fields) = fields_by_item.get(item.item_id.as_str()) {
md.push_str("| Label | Type | Value |\n");
md.push_str("|-------|------|-------|\n");
for f in item_fields {
let escaped_value = f.value.replace('|', "\\|");
md.push_str(&format!(
"| {} | {} | {} |\n",
f.label, f.field_type, escaped_value
));
}
} else {
md.push_str("*(no fields)*\n");
}
md.push('\n');
}
md.push_str("## Deleted Records\n\n");
if sorted_deleted.is_empty() {
md.push_str("*(none)*\n\n");
} else {
for item in &sorted_deleted {
let path = compute_path(item);
md.push_str(&format!(
"### {} (id: {}, icon: {}, folder: {})\n\n",
item.name,
item.item_id,
item.icon,
if item.folder { "yes" } else { "no" }
));
md.push_str(&format!("Path: `{}`\n\n", path));
if let Some(item_fields) = deleted_fields_by_item.get(item.item_id.as_str()) {
md.push_str("| Label | Type | Value |\n");
md.push_str("|-------|------|-------|\n");
for f in item_fields {
let escaped_value = f.value.replace('|', "\\|");
md.push_str(&format!(
"| {} | {} | {} |\n",
f.label, f.field_type, escaped_value
));
}
} else {
md.push_str("*(no fields)*\n");
}
md.push('\n');
}
}
let total_items = items.len() - 1; let folder_count = items.iter().filter(|i| i.folder && !i.is_root()).count();
let document_count = items.iter().filter(|i| !i.folder && !i.is_root()).count();
let total_fields = fields.len();
let total_deleted_items = deleted_items.len();
let total_deleted_fields = deleted_fields.len();
let total_labels = labels.len();
let system_labels = labels.iter().filter(|l| l.system).count();
let custom_labels = labels.iter().filter(|l| !l.system).count();
md.push_str("## Statistics\n\n");
md.push_str("| Metric | Count |\n");
md.push_str("|--------|-------|\n");
md.push_str(&format!("| Total active items | {} |\n", total_items));
md.push_str(&format!("| Folders | {} |\n", folder_count));
md.push_str(&format!("| Documents | {} |\n", document_count));
md.push_str(&format!("| Total active fields | {} |\n", total_fields));
md.push_str(&format!("| Deleted items | {} |\n", total_deleted_items));
md.push_str(&format!("| Deleted fields | {} |\n", total_deleted_fields));
md.push_str(&format!("| Total labels | {} |\n", total_labels));
md.push_str(&format!("| System labels | {} |\n", system_labels));
md.push_str(&format!("| Custom labels | {} |\n", custom_labels));
let output_path = testdata_dir().join("nswallet.dat.md");
fs::write(&output_path, &md).expect("Failed to write nswallet.dat.md");
println!("Wrote {}", output_path.display());
}
fn dump_version_only(db_filename: &str) {
let db_path = testdata_dir().join(db_filename);
assert!(db_path.exists(), "Test database not found: {:?}", db_path);
let version = get_db_version(&db_path).unwrap();
let compatible = check_db_version(&db_path).unwrap();
let mut md = String::new();
md.push_str(&format!("# {}\n\n", db_filename));
md.push_str("> Auto-generated by `cargo test dump_database_contents`\n\n");
md.push_str("| Property | Value |\n");
md.push_str("|----------|-------|\n");
md.push_str(&format!("| Version | {} |\n", version));
md.push_str(&format!(
"| Compatible | {} |\n",
if compatible { "yes" } else { "no" }
));
let output_path = testdata_dir().join(format!("{}.md", db_filename));
fs::write(&output_path, &md).expect(&format!("Failed to write {}.md", db_filename));
println!("Wrote {}", output_path.display());
}