diff --git a/package.json b/package.json index 823d2801..3a1492cb 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dev": "vite", "build": "tsc && vite build", "release": "node scripts/sync-version.mjs --commit && tsc && vite build && tauri build", + "build:unlock-rescue": "powershell -ExecutionPolicy Bypass -File scripts/build-unlock-rescue.ps1", "preview": "vite preview", "tauri": "tauri", "lint": "node scripts/check-no-console.mjs && biome check src/", diff --git a/scripts/build-unlock-rescue.ps1 b/scripts/build-unlock-rescue.ps1 new file mode 100644 index 00000000..013cd10c --- /dev/null +++ b/scripts/build-unlock-rescue.ps1 @@ -0,0 +1,18 @@ +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$manifestPath = Join-Path $repoRoot "src-tauri/Cargo.toml" +$artifactDir = Join-Path $repoRoot "dist/rescue" +$sourceExe = Join-Path $repoRoot "src-tauri/target/release/nyaterm-unlock-rescue.exe" +$targetExe = Join-Path $artifactDir "nyaterm-unlock-rescue.exe" +$targetZip = Join-Path $artifactDir "nyaterm-unlock-rescue-windows-x64.zip" + +cargo build --release --manifest-path $manifestPath --bin nyaterm-unlock-rescue + +New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null +Copy-Item -Force -Path $sourceExe -Destination $targetExe +Compress-Archive -Force -Path $targetExe -DestinationPath $targetZip + +Write-Host "Built rescue tool:" +Write-Host $targetExe +Write-Host $targetZip diff --git a/src-tauri/examples/nyaterm-unlock-rescue.rs b/src-tauri/examples/nyaterm-unlock-rescue.rs new file mode 100644 index 00000000..de823f1f --- /dev/null +++ b/src-tauri/examples/nyaterm-unlock-rescue.rs @@ -0,0 +1,112 @@ +use redb::{Database, ReadableTable, TableDefinition}; +use serde_json::Value; +use std::path::PathBuf; + +const SETTINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("settings"); +const SETTINGS_KEY: &str = "settings/default"; + +fn main() { + if let Err(error) = run() { + eprintln!("NyaTerm unlock rescue failed: {error}"); + wait_for_enter(); + std::process::exit(1); + } + + wait_for_enter(); +} + +fn run() -> Result<(), Box> { + let db_path = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(default_db_path); + + if !db_path.exists() { + return Err(format!("database not found: {}", db_path.display()).into()); + } + + let backup_path = db_path.with_extension(format!( + "redb.screen-lock-rescue-{}.bak", + unix_timestamp_seconds() + )); + std::fs::copy(&db_path, &backup_path)?; + + let db = Database::open(&db_path)?; + let txn = db.begin_write()?; + let changed = { + let mut table = txn.open_table(SETTINGS_TABLE)?; + let (mut settings, was_enabled, old_idle): (Value, bool, u64) = { + let Some(raw) = table.get(SETTINGS_KEY)? else { + return Err(format!("settings document not found: {SETTINGS_KEY}").into()); + }; + + let mut settings: Value = serde_json::from_slice(raw.value())?; + let security = settings + .as_object_mut() + .and_then(|root| root.get_mut("security")) + .and_then(Value::as_object_mut) + .ok_or("settings.security is missing or not an object")?; + + let was_enabled = security + .get("enable_screen_lock") + .and_then(Value::as_bool) + .unwrap_or(false); + let old_idle = security + .get("idle_lock_minutes") + .and_then(Value::as_u64) + .unwrap_or(0); + + (settings, was_enabled, old_idle) + }; + + let security = settings + .as_object_mut() + .and_then(|root| root.get_mut("security")) + .and_then(Value::as_object_mut) + .ok_or("settings.security is missing or not an object")?; + + security.insert("enable_screen_lock".to_string(), Value::Bool(false)); + security.insert( + "idle_lock_minutes".to_string(), + Value::Number(serde_json::Number::from(0)), + ); + + let content = serde_json::to_vec(&settings)?; + table.insert(SETTINGS_KEY, content.as_slice())?; + was_enabled || old_idle != 0 + }; + txn.commit()?; + + println!("NyaTerm unlock rescue completed."); + println!("Database: {}", db_path.display()); + println!("Backup: {}", backup_path.display()); + println!("Screen lock disabled: {changed}"); + println!("Master password was left unchanged."); + println!("You can start NyaTerm again now."); + + Ok(()) +} + +fn default_db_path() -> PathBuf { + std::env::var_os("USERPROFILE") + .or_else(|| std::env::var_os("HOME")) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")) + .join(".nyaterm") + .join("nyaterm.redb") +} + +fn unix_timestamp_seconds() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or(0) +} + +fn wait_for_enter() { + println!(); + println!("Press Enter to close this window."); + + let mut input = String::new(); + let _ = std::io::stdin().read_line(&mut input); +}