use rusqlite::{Connection, params};
use chrono::{DateTime, Utc};
use crate::error::Result;
pub const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
pub fn format_timestamp(dt: &DateTime<Utc>) -> String {
dt.format(TIMESTAMP_FORMAT).to_string()
}
pub fn parse_timestamp(s: &str) -> Option<DateTime<Utc>> {
chrono::NaiveDateTime::parse_from_str(s, TIMESTAMP_FORMAT)
.ok()
.map(|ndt| DateTime::from_naive_utc_and_offset(ndt, Utc))
}
pub fn now_timestamp() -> String {
format_timestamp(&Utc::now())
}
pub fn get_database_id(conn: &Connection) -> Result<Option<String>> {
let result = conn.query_row(
"SELECT database_id FROM nswallet_properties LIMIT 1",
[],
|row| row.get(0),
);
Ok(result.ok())
}
pub fn has_properties(conn: &Connection) -> Result<bool> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_properties",
[],
|row| row.get(0),
)?;
Ok(count > 0)
}
pub fn get_properties(conn: &Connection) -> Result<Option<RawProperties>> {
let result = conn.query_row(
"SELECT database_id, lang, version, email, sync_timestamp, update_timestamp
FROM nswallet_properties LIMIT 1",
[],
|row| {
Ok(RawProperties {
database_id: row.get(0)?,
lang: row.get(1)?,
version: row.get(2)?,
email: row.get(3)?,
sync_timestamp: row.get(4)?,
update_timestamp: row.get(5)?,
})
},
);
Ok(result.ok())
}
pub fn set_properties(
conn: &Connection,
database_id: &str,
lang: &str,
version: &str,
encryption_count: u32,
) -> Result<()> {
conn.execute(
"INSERT INTO nswallet_properties (database_id, lang, version, email, sync_timestamp, update_timestamp)
VALUES (?, ?, ?, ?, ?, ?)",
params![database_id, lang, version, encryption_count.to_string(), now_timestamp(), now_timestamp()],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn set_db_version(conn: &Connection, version: &str) -> Result<()> {
conn.execute(
"UPDATE nswallet_properties SET version = ?, update_timestamp = ?",
params![version, now_timestamp()],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn get_all_items_raw(conn: &Connection) -> Result<Vec<RawItem>> {
let mut stmt = conn.prepare(
"SELECT item_id, parent_id, name, icon, folder, create_timestamp, change_timestamp, deleted
FROM nswallet_items WHERE deleted = 0"
)?;
let items = stmt.query_map([], |row| {
Ok(RawItem {
item_id: row.get(0)?,
parent_id: row.get(1)?,
name_encrypted: row.get(2)?,
icon: row.get(3)?,
folder: row.get::<_, i32>(4)? != 0,
create_timestamp: row.get(5)?,
change_timestamp: row.get(6)?,
deleted: row.get::<_, i32>(7)? != 0,
})
})?;
items.collect::<std::result::Result<Vec<_>, _>>().map_err(Into::into)
}
pub fn get_root_item_raw(conn: &Connection) -> Result<Option<Vec<u8>>> {
let result = conn.query_row(
"SELECT name FROM nswallet_items WHERE item_id = '__ROOT__'",
[],
|row| row.get(0),
);
Ok(result.ok())
}
pub fn has_root_item(conn: &Connection) -> Result<bool> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_items WHERE item_id = '__ROOT__'",
[],
|row| row.get(0),
)?;
Ok(count > 0)
}
pub fn create_item(
conn: &Connection,
item_id: &str,
parent_id: &str,
name_encrypted: &[u8],
icon: &str,
folder: bool,
) -> Result<()> {
let now = now_timestamp();
conn.execute(
"INSERT INTO nswallet_items (item_id, parent_id, name, icon, folder, create_timestamp, change_timestamp, deleted)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)",
params![item_id, parent_id, name_encrypted, icon, folder as i32, now, now],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_item_name(conn: &Connection, item_id: &str, name_encrypted: &[u8]) -> Result<()> {
conn.execute(
"UPDATE nswallet_items SET name = ?, change_timestamp = ? WHERE item_id = ?",
params![name_encrypted, now_timestamp(), item_id],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_item_icon(conn: &Connection, item_id: &str, icon: &str) -> Result<()> {
conn.execute(
"UPDATE nswallet_items SET icon = ?, change_timestamp = ? WHERE item_id = ?",
params![icon, now_timestamp(), item_id],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_item_parent(conn: &Connection, item_id: &str, parent_id: &str) -> Result<()> {
conn.execute(
"UPDATE nswallet_items SET parent_id = ?, change_timestamp = ? WHERE item_id = ?",
params![parent_id, now_timestamp(), item_id],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn delete_item(conn: &Connection, item_id: &str) -> Result<()> {
let now = now_timestamp();
conn.execute(
"UPDATE nswallet_items SET deleted = 1, change_timestamp = ? WHERE item_id = ?",
params![now, item_id],
)?;
conn.execute(
"UPDATE nswallet_fields SET deleted = 1, change_timestamp = ? WHERE item_id = ? AND deleted = 0",
params![now, item_id],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn get_deleted_items_raw(conn: &Connection) -> Result<Vec<RawItem>> {
let mut stmt = conn.prepare(
"SELECT item_id, parent_id, name, COALESCE(icon, ''), folder, create_timestamp, change_timestamp, deleted
FROM nswallet_items WHERE deleted = 1"
)?;
let items = stmt.query_map([], |row| {
Ok(RawItem {
item_id: row.get(0)?,
parent_id: row.get(1)?,
name_encrypted: row.get(2)?,
icon: row.get(3)?,
folder: row.get::<_, i32>(4)? != 0,
create_timestamp: row.get(5)?,
change_timestamp: row.get(6)?,
deleted: row.get::<_, i32>(7)? != 0,
})
})?;
items.collect::<std::result::Result<Vec<_>, _>>().map_err(Into::into)
}
pub fn undelete_item(conn: &Connection, item_id: &str) -> Result<()> {
let rows = conn.execute(
"UPDATE nswallet_items SET deleted = 0, parent_id = '__ROOT__', change_timestamp = ? WHERE item_id = ? AND deleted = 1",
params![now_timestamp(), item_id],
)?;
if rows == 0 {
return Err(crate::error::WalletError::ItemNotFound(item_id.to_string()));
}
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn delete_item_descendants(conn: &Connection, item_id: &str) -> Result<()> {
let now = now_timestamp();
conn.execute(
"WITH RECURSIVE descendants(id) AS (
SELECT item_id FROM nswallet_items WHERE parent_id = ?1 AND deleted = 0
UNION ALL
SELECT i.item_id FROM nswallet_items i JOIN descendants d ON i.parent_id = d.id WHERE i.deleted = 0
)
UPDATE nswallet_items SET deleted = 1, change_timestamp = ?2 WHERE item_id IN (SELECT id FROM descendants)",
params![item_id, now],
)?;
conn.execute(
"WITH RECURSIVE descendants(id) AS (
SELECT item_id FROM nswallet_items WHERE parent_id = ?1
UNION ALL
SELECT i.item_id FROM nswallet_items i JOIN descendants d ON i.parent_id = d.id
)
UPDATE nswallet_fields SET deleted = 1, change_timestamp = ?2 WHERE item_id IN (SELECT id FROM descendants) AND deleted = 0",
params![item_id, now],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_item_name_only(conn: &Connection, item_id: &str, name_encrypted: &[u8]) -> Result<()> {
conn.execute(
"UPDATE nswallet_items SET name = ? WHERE item_id = ?",
params![name_encrypted, item_id],
)?;
Ok(())
}
pub fn get_all_fields_raw(conn: &Connection) -> Result<Vec<RawField>> {
let mut stmt = conn.prepare(
"SELECT item_id, field_id, type, value, change_timestamp, deleted, sort_weight
FROM nswallet_fields WHERE deleted = 0"
)?;
let fields = stmt.query_map([], |row| {
Ok(RawField {
item_id: row.get(0)?,
field_id: row.get(1)?,
field_type: row.get(2)?,
value_encrypted: row.get(3)?,
change_timestamp: row.get(4)?,
deleted: row.get::<_, i32>(5)? != 0,
sort_weight: row.get(6)?,
})
})?;
fields.collect::<std::result::Result<Vec<_>, _>>().map_err(Into::into)
}
pub fn create_field(
conn: &Connection,
item_id: &str,
field_id: &str,
field_type: &str,
value_encrypted: &[u8],
sort_weight: i32,
) -> Result<()> {
conn.execute(
"INSERT INTO nswallet_fields (item_id, field_id, type, value, change_timestamp, deleted, sort_weight)
VALUES (?, ?, ?, ?, ?, 0, ?)",
params![item_id, field_id, field_type, value_encrypted, now_timestamp(), sort_weight],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_field(
conn: &Connection,
field_id: &str,
value_encrypted: &[u8],
sort_weight: Option<i32>,
) -> Result<()> {
if let Some(weight) = sort_weight {
conn.execute(
"UPDATE nswallet_fields SET value = ?, sort_weight = ?, change_timestamp = ? WHERE field_id = ?",
params![value_encrypted, weight, now_timestamp(), field_id],
)?;
} else {
conn.execute(
"UPDATE nswallet_fields SET value = ?, change_timestamp = ? WHERE field_id = ?",
params![value_encrypted, now_timestamp(), field_id],
)?;
}
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn delete_field(conn: &Connection, item_id: &str, field_id: &str) -> Result<()> {
conn.execute(
"UPDATE nswallet_fields SET deleted = 1, change_timestamp = ? WHERE item_id = ? AND field_id = ?",
params![now_timestamp(), item_id, field_id],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_field_value_only(conn: &Connection, item_id: &str, field_id: &str, value_encrypted: &[u8]) -> Result<()> {
conn.execute(
"UPDATE nswallet_fields SET value = ? WHERE item_id = ? AND field_id = ?",
params![value_encrypted, item_id, field_id],
)?;
Ok(())
}
pub fn get_deleted_fields_raw(conn: &Connection) -> Result<Vec<RawField>> {
let mut stmt = conn.prepare(
"SELECT item_id, field_id, type, value, change_timestamp, deleted, sort_weight
FROM nswallet_fields WHERE deleted = 1"
)?;
let fields = stmt.query_map([], |row| {
Ok(RawField {
item_id: row.get(0)?,
field_id: row.get(1)?,
field_type: row.get(2)?,
value_encrypted: row.get(3)?,
change_timestamp: row.get(4)?,
deleted: row.get::<_, i32>(5)? != 0,
sort_weight: row.get(6)?,
})
})?;
fields.collect::<std::result::Result<Vec<_>, _>>().map_err(Into::into)
}
pub fn undelete_field(conn: &Connection, item_id: &str, field_id: &str) -> Result<()> {
let rows = conn.execute(
"UPDATE nswallet_fields SET deleted = 0, change_timestamp = ? WHERE item_id = ? AND field_id = ? AND deleted = 1",
params![now_timestamp(), item_id, field_id],
)?;
if rows == 0 {
return Err(crate::error::WalletError::FieldNotFound(field_id.to_string()));
}
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn purge_deleted(conn: &Connection) -> Result<(u32, u32)> {
let items_count: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_items WHERE deleted = 1",
[],
|row| row.get(0),
)?;
let fields_count: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_fields WHERE deleted = 1 OR item_id IN (SELECT item_id FROM nswallet_items WHERE deleted = 1)",
[],
|row| row.get(0),
)?;
conn.execute(
"DELETE FROM nswallet_fields WHERE item_id IN (SELECT item_id FROM nswallet_items WHERE deleted = 1)",
[],
)?;
conn.execute(
"DELETE FROM nswallet_fields WHERE deleted = 1",
[],
)?;
conn.execute(
"DELETE FROM nswallet_items WHERE deleted = 1",
[],
)?;
conn.execute(
"DELETE FROM nswallet_labels WHERE deleted = 1",
[],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok((items_count, fields_count))
}
#[derive(Debug, Clone)]
pub struct DatabaseStats {
pub total_items: u32,
pub total_folders: u32,
pub total_fields: u32,
pub total_labels: u32,
pub custom_labels: u32,
pub deleted_items: u32,
pub deleted_fields: u32,
pub file_size_bytes: u64,
}
pub fn get_database_stats(conn: &Connection) -> Result<DatabaseStats> {
let total_items: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_items WHERE deleted = 0 AND item_id != '__ROOT__' AND folder = 0",
[],
|row| row.get(0),
)?;
let total_folders: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_items WHERE deleted = 0 AND item_id != '__ROOT__' AND folder = 1",
[],
|row| row.get(0),
)?;
let total_fields: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_fields WHERE deleted = 0",
[],
|row| row.get(0),
)?;
let total_labels: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_labels WHERE deleted = 0",
[],
|row| row.get(0),
)?;
let custom_labels: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_labels WHERE deleted = 0 AND system = 0",
[],
|row| row.get(0),
)?;
let deleted_items: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_items WHERE deleted = 1",
[],
|row| row.get(0),
)?;
let deleted_fields: u32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_fields WHERE deleted = 1",
[],
|row| row.get(0),
)?;
Ok(DatabaseStats {
total_items,
total_folders,
total_fields,
total_labels,
custom_labels,
deleted_items,
deleted_fields,
file_size_bytes: 0, })
}
pub fn get_field_raw_by_id(conn: &Connection, field_id: &str) -> Result<Option<RawField>> {
let result = conn.query_row(
"SELECT item_id, field_id, type, value, change_timestamp, deleted, sort_weight
FROM nswallet_fields WHERE field_id = ? AND deleted = 0",
params![field_id],
|row| {
Ok(RawField {
item_id: row.get(0)?,
field_id: row.get(1)?,
field_type: row.get(2)?,
value_encrypted: row.get(3)?,
change_timestamp: row.get(4)?,
deleted: row.get::<_, i32>(5)? != 0,
sort_weight: row.get(6)?,
})
},
);
Ok(result.ok())
}
pub fn get_oldp_field_id(conn: &Connection, item_id: &str) -> Result<Option<String>> {
let result = conn.query_row(
"SELECT field_id FROM nswallet_fields WHERE item_id = ? AND type = 'OLDP' AND deleted = 0",
params![item_id],
|row| row.get(0),
);
Ok(result.ok())
}
pub fn get_max_field_weight(conn: &Connection, item_id: &str) -> Result<i32> {
let result: Option<i32> = conn.query_row(
"SELECT MAX(sort_weight) FROM nswallet_fields WHERE item_id = ? AND deleted = 0",
params![item_id],
|row| row.get(0),
).ok().flatten();
Ok(result.unwrap_or(0))
}
pub fn get_all_labels(conn: &Connection) -> Result<Vec<RawLabel>> {
let mut stmt = conn.prepare(
"SELECT l.field_type, l.label_name, l.value_type, l.icon, l.system, l.change_timestamp, l.deleted,
COALESCE((SELECT COUNT(*) FROM nswallet_fields f WHERE f.type = l.field_type AND f.deleted = 0), 0) as usage
FROM nswallet_labels l WHERE l.deleted = 0"
)?;
let labels = stmt.query_map([], |row| {
Ok(RawLabel {
field_type: row.get(0)?,
label_name: row.get(1)?,
value_type: row.get(2)?,
icon: row.get(3)?,
system: row.get::<_, i32>(4)? != 0,
change_timestamp: row.get(5)?,
deleted: row.get::<_, i32>(6)? != 0,
usage: row.get(7)?,
})
})?;
labels.collect::<std::result::Result<Vec<_>, _>>().map_err(Into::into)
}
pub fn create_label(
conn: &Connection,
field_type: &str,
label_name: &str,
value_type: &str,
icon: &str,
system: bool,
) -> Result<bool> {
let result = conn.execute(
"INSERT OR IGNORE INTO nswallet_labels (field_type, label_name, value_type, icon, system, change_timestamp, deleted)
VALUES (?, ?, ?, ?, ?, ?, 0)",
params![field_type, label_name, value_type, icon, system as i32, now_timestamp()],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(result > 0)
}
pub fn update_label_name(conn: &Connection, field_type: &str, label_name: &str) -> Result<()> {
conn.execute(
"UPDATE nswallet_labels SET label_name = ?, change_timestamp = ? WHERE field_type = ?",
params![label_name, now_timestamp(), field_type],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn update_label_icon(conn: &Connection, field_type: &str, icon: &str) -> Result<()> {
conn.execute(
"UPDATE nswallet_labels SET icon = ?, change_timestamp = ? WHERE field_type = ?",
params![icon, now_timestamp(), field_type],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(())
}
pub fn delete_label(conn: &Connection, field_type: &str) -> Result<i32> {
let count: i32 = conn.query_row(
"SELECT COUNT(*) FROM nswallet_fields WHERE type = ? AND deleted = 0",
params![field_type],
|row| row.get(0),
)?;
if count == 0 {
conn.execute(
"UPDATE nswallet_labels SET deleted = 1, change_timestamp = ? WHERE field_type = ?",
params![now_timestamp(), field_type],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
}
Ok(count)
}
pub fn remove_label_for_real(conn: &Connection, field_type: &str) -> Result<bool> {
let result = conn.execute(
"DELETE FROM nswallet_labels WHERE field_type = ?",
params![field_type],
)?;
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(result > 0)
}
#[derive(Debug, Clone)]
pub struct RawProperties {
pub database_id: String,
pub lang: String,
pub version: String,
pub email: String,
pub sync_timestamp: Option<String>,
pub update_timestamp: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RawItem {
pub item_id: String,
pub parent_id: Option<String>,
pub name_encrypted: Vec<u8>,
pub icon: String,
pub folder: bool,
pub create_timestamp: Option<String>,
pub change_timestamp: Option<String>,
pub deleted: bool,
}
#[derive(Debug, Clone)]
pub struct RawField {
pub item_id: String,
pub field_id: String,
pub field_type: String,
pub value_encrypted: Vec<u8>,
pub change_timestamp: Option<String>,
pub deleted: bool,
pub sort_weight: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct RawLabel {
pub field_type: String,
pub label_name: String,
pub value_type: String,
pub icon: String,
pub system: bool,
pub change_timestamp: Option<String>,
pub deleted: bool,
pub usage: i32,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Datelike, Timelike};
#[test]
fn test_format_timestamp() {
let dt = Utc.with_ymd_and_hms(2023, 12, 15, 10, 30, 45).unwrap();
assert_eq!(format_timestamp(&dt), "2023-12-15 10:30:45");
}
#[test]
fn test_parse_timestamp() {
let ts = parse_timestamp("2023-12-15 10:30:45").unwrap();
assert_eq!(ts.year(), 2023);
assert_eq!(ts.month(), 12);
assert_eq!(ts.day(), 15);
assert_eq!(ts.hour(), 10);
assert_eq!(ts.minute(), 30);
assert_eq!(ts.second(), 45);
}
#[test]
fn test_parse_timestamp_invalid() {
assert!(parse_timestamp("invalid").is_none());
assert!(parse_timestamp("2023-13-01 00:00:00").is_none());
}
#[test]
fn test_now_timestamp() {
let ts = now_timestamp();
assert_eq!(ts.len(), 19);
assert!(ts.contains("-"));
assert!(ts.contains(":"));
}
}