#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::fmt::Write as _;
use std::path::PathBuf;
use std::process::Command;
use sqlite_core::{decode_localstorage_value, is_local_storage_item_table, Database, Value};
fn sqlite3_bin() -> Option<String> {
let bin = std::env::var("SQLITE3_BIN").unwrap_or_else(|_| "sqlite3".to_string());
Command::new(&bin)
.arg("--version")
.output()
.ok()
.filter(|o| o.status.success())
.map(|_| bin)
}
fn temp_path(tag: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!("sqlite4n6_localstorage_{tag}_{nonce}.localstorage"));
p
}
fn utf16le_hex(s: &str) -> String {
let mut out = String::new();
for byte in s.encode_utf16().flat_map(u16::to_le_bytes) {
let _ = write!(out, "{byte:02x}");
}
out
}
fn run_sql(bin: &str, db: &PathBuf, sql: &str) {
let out = Command::new(bin)
.arg(db)
.arg(sql)
.output()
.expect("sqlite3 must execute");
assert!(
out.status.success(),
"sqlite3 failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn item_table_utf16le_blob_values_round_trip() {
let Some(bin) = sqlite3_bin() else {
eprintln!("SKIP item_table_utf16le_blob_values_round_trip: no sqlite3 (set SQLITE3_BIN)");
return;
};
let cases = [
("token", "hello"),
("accents", "héllo wörld"),
("cjk", "中文字符串"),
("emoji", "party 😀🎉 time"),
];
let mut sql = String::from(
"CREATE TABLE ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, \
value BLOB NOT NULL ON CONFLICT FAIL);",
);
for (k, v) in cases {
let _ = write!(
sql,
"INSERT INTO ItemTable VALUES('{k}', x'{}');",
utf16le_hex(v)
);
}
let path = temp_path("roundtrip");
run_sql(&bin, &path, &sql);
let bytes = std::fs::read(&path).unwrap();
let db = Database::open(bytes).expect("a .localstorage is a standard SQLite db");
let dumps = db.live_table_rows();
let item = dumps
.iter()
.find(|d| is_local_storage_item_table(&d.name))
.expect("ItemTable schema recognized");
let mut recovered = std::collections::BTreeMap::new();
for row in &item.rows {
let key = match &row.values[0] {
Value::Text(s) => s.clone(),
other => panic!("key column should be TEXT, got {other:?}"),
};
let decoded = match &row.values[1] {
Value::Blob(b) => decode_localstorage_value(b),
other => panic!("value column should be a BLOB, got {other:?}"),
};
assert!(
!decoded.lossy,
"a clean UTF-16-LE value must not be lossy: {key}"
);
recovered.insert(key, decoded.text);
}
for (k, v) in cases {
assert_eq!(
recovered.get(k).map(String::as_str),
Some(v),
"exact UTF-16-LE round-trip for key {k}"
);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn item_table_null_value_reads_as_null_not_decoded() {
let Some(bin) = sqlite3_bin() else {
eprintln!(
"SKIP item_table_null_value_reads_as_null_not_decoded: no sqlite3 (set SQLITE3_BIN)"
);
return;
};
let path = temp_path("null");
run_sql(
&bin,
&path,
"CREATE TABLE ItemTable (key TEXT, value BLOB); \
INSERT INTO ItemTable VALUES('present', x'68006900'); \
INSERT INTO ItemTable VALUES('missing', NULL);",
);
let bytes = std::fs::read(&path).unwrap();
let db = Database::open(bytes).unwrap();
let dumps = db.live_table_rows();
let item = dumps
.iter()
.find(|d| is_local_storage_item_table(&d.name))
.expect("ItemTable schema recognized");
let mut present_decoded = false;
let mut missing_not_blob = false;
for row in &item.rows {
let key = match &row.values[0] {
Value::Text(s) => s.clone(),
other => panic!("key column should be TEXT, got {other:?}"),
};
let value = row.values.get(1);
match key.as_str() {
"present" => match value {
Some(Value::Blob(b)) => {
let decoded = decode_localstorage_value(b);
assert_eq!(decoded.text, "hi");
assert!(!decoded.lossy);
present_decoded = true;
}
other => panic!("present value should be a BLOB, got {other:?}"),
},
"missing" => {
assert!(
!matches!(value, Some(Value::Blob(_))),
"a NULL value must not surface as a BLOB"
);
missing_not_blob = true;
}
_ => {}
}
}
assert!(present_decoded, "the present BLOB value decoded");
assert!(missing_not_blob, "the NULL value was not a decodable BLOB");
let _ = std::fs::remove_file(&path);
}