chore: replace json config with database

This commit is contained in:
HorizonCode committed 2026-05-28 11:35:34 +02:00
1 parent 2ba094f07e
commit 20e8389409
16 files changed
+761 -226

No files matched your search

+41 -3
View File
@@ -1,6 +1,8 @@
use crate::config::ConfigError;
use hardware_id::get_id;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::io::Read;
use std::path::PathBuf;
@@ -8,15 +10,16 @@ use sysinfo::System;
use tauri::AppHandle;
use tauri::Emitter;
use tauri::Manager;
use tauri::State;
use tauri_plugin_fs::FsExt;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::time::{Duration, sleep};
use crate::OpenedUrls;
use crate::osudb::parse_osudb;
use crate::presence;
use crate::state::AppState;
use crate::utils::{
check_folder_completeness, encrypt_password, get_osu_config, get_osu_user_config,
get_window_title_by_pid, is_net8_installed, is_osuwinello_available, is_wmctrl_available,
@@ -25,8 +28,8 @@ use crate::utils::{
#[tauri::command]
pub fn opened_urls(app: tauri::AppHandle) -> Vec<PathBuf> {
let args = app.state::<OpenedUrls>().0.lock().unwrap().clone();
app.state::<OpenedUrls>().0.lock().unwrap().clear();
let args = app.state::<AppState>().opened_files.lock().unwrap().clone();
app.state::<AppState>().opened_files.lock().unwrap().clear();
args
}
@@ -1296,3 +1299,38 @@ pub async fn read_theme_info(app: AppHandle, file_path: String) -> Result<ThemeI
.await
.map_err(|e| format!("Thread error: {}", e))?
}
#[tauri::command]
pub fn config_get(key: String, state: State<AppState>) -> Result<Option<Value>, ConfigError> {
state.config.lock().unwrap().get(&key)
}
#[tauri::command]
pub fn config_set(
key: String,
value: Value,
encrypt: bool,
state: State<AppState>,
) -> Result<(), ConfigError> {
state.config.lock().unwrap().set(&key, value, encrypt)
}
#[tauri::command]
pub fn config_exists(key: String, state: State<AppState>) -> Result<bool, ConfigError> {
state.config.lock().unwrap().exists(&key)
}
#[tauri::command]
pub fn config_delete(key: String, state: State<AppState>) -> Result<(), ConfigError> {
state.config.lock().unwrap().delete(&key)
}
#[tauri::command]
pub fn config_clear(state: State<AppState>) -> Result<(), ConfigError> {
state.config.lock().unwrap().clear()
}
#[tauri::command]
pub fn config_all(state: State<AppState>) -> Result<Vec<(String, Value)>, ConfigError> {
state.config.lock().unwrap().all()
}
+219
View File
@@ -0,0 +1,219 @@
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rusqlite::{Connection, OptionalExtension, params};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("SQLite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("Encryption error: {0}")]
Encryption(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Base64 error: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Config not initialized")]
NotInitialized,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl serde::Serialize for ConfigError {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
pub struct Config {
db_path: PathBuf,
conn: Option<Connection>,
hwid_key: Option<[u8; 32]>,
}
impl Config {
pub fn new(db_path: impl Into<PathBuf>) -> Self {
Self {
db_path: db_path.into(),
conn: None,
hwid_key: None,
}
}
pub fn init(&mut self, hwid: &str) -> Result<bool, ConfigError> {
let mut hasher = Sha256::new();
hasher.update(hwid.as_bytes());
let key_bytes: [u8; 32] = hasher.finalize().into();
self.hwid_key = Some(key_bytes);
let is_new = !self.db_path.exists();
if let Some(parent) = self.db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(&self.db_path)?;
conn.execute_batch(
"
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
encrypted INTEGER NOT NULL DEFAULT 0
);
",
)?;
self.conn = Some(conn);
Ok(is_new)
}
pub fn get(&self, key: &str) -> Result<Option<Value>, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let mut stmt = conn.prepare("SELECT value, encrypted FROM config WHERE key = ?1")?;
let row: Option<(String, bool)> = stmt
.query_row(params![key], |r| Ok((r.get(0)?, r.get(1)?)))
.optional()?;
match row {
None => Ok(None),
Some((raw, false)) => Ok(Some(serde_json::from_str::<Value>(&raw)?)),
Some((raw, true)) => {
let plaintext = self
.decrypt_value(&raw)
.map_err(|_| ConfigError::Encryption(format!("failed to decrypt '{}'", key)))?;
serde_json::from_str::<Value>(&plaintext)
.map(Some)
.map_err(|_| {
ConfigError::Encryption(format!("decrypted '{}' is not valid JSON", key))
})
}
}
}
pub fn exists(&self, key: &str) -> Result<bool, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM config WHERE key = ?1",
params![key],
|r| r.get(0),
)?;
Ok(count > 0)
}
/// Return all key/value pairs (decrypting encrypted ones).
pub fn all(&self) -> Result<Vec<(String, Value)>, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let mut stmt = conn.prepare("SELECT key, value, encrypted FROM config")?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, bool>(2)?,
))
})?;
let mut out = Vec::new();
for row in rows {
let (k, raw, enc) = row?;
let v: Value = if enc {
let pt = self.decrypt_value(&raw)?;
serde_json::from_str(&pt)?
} else {
serde_json::from_str(&raw)?
};
out.push((k, v));
}
Ok(out)
}
pub fn set<T: serde::Serialize>(
&self,
key: &str,
value: T,
encrypt: bool,
) -> Result<(), ConfigError> {
let json = serde_json::to_string(&value)?;
if encrypt {
let blob = self.encrypt_value(&json)?;
self.upsert(key, &blob, true)
} else {
self.upsert(key, &json, false)
}
}
pub fn delete(&self, key: &str) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute("DELETE FROM config WHERE key = ?1", params![key])?;
Ok(())
}
pub fn clear(&self) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute("DELETE FROM config", [])?;
Ok(())
}
fn upsert(&self, key: &str, raw: &str, encrypted: bool) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute(
"INSERT INTO config (key, value, encrypted)
VALUES (?1, ?2, ?3)
ON CONFLICT(key) DO UPDATE SET value = excluded.value,
encrypted = excluded.encrypted",
params![key, raw, encrypted as i32],
)?;
Ok(())
}
fn cipher(&self) -> Result<Aes256Gcm, ConfigError> {
let key_bytes = self.hwid_key.ok_or(ConfigError::NotInitialized)?;
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
Ok(Aes256Gcm::new(key))
}
fn encrypt_value(&self, plaintext: &str) -> Result<String, ConfigError> {
let cipher = self.cipher()?;
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| ConfigError::Encryption(e.to_string()))?;
let mut combined = nonce_bytes.to_vec();
combined.extend_from_slice(&ciphertext);
Ok(BASE64.encode(&combined))
}
fn decrypt_value(&self, blob: &str) -> Result<String, ConfigError> {
let cipher = self.cipher()?;
let combined = BASE64.decode(blob)?;
if combined.len() < 12 {
return Err(ConfigError::Encryption("blob too short".into()));
}
let (nonce_bytes, ciphertext) = combined.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| ConfigError::Encryption(e.to_string()))?;
String::from_utf8(plaintext).map_err(|e| ConfigError::Encryption(e.to_string()))
}
}
+44 -20
View File
@@ -1,27 +1,32 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use hardware_id::get_id;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::Manager;
use tauri_plugin_fs::FsExt;
pub mod commands;
pub mod config;
pub mod osudb;
pub mod presence;
pub mod state;
pub mod utils;
use crate::commands::{
check_for_corruption, download_and_extract_theme, download_ezpp_launcher_update,
download_ezpp_launcher_update_files, encrypt_string, exit, extract_theme,
find_osu_installation, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid,
get_launcher_version, get_osu_release_stream, get_osu_skin, get_osu_version, get_platform,
get_skins, get_skins_count, has_net8, has_osuwinello, has_wmctrl, install_ezpp_launcher_update,
is_osu_running, open_url_in_browser, opened_urls, presence_connect, presence_disconnect,
presence_is_connected, presence_update_button, presence_update_status, presence_update_user,
read_theme_info, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
set_osu_user_config_values, valid_osu_folder,
use crate::{
commands::{
check_for_corruption, config_all, config_clear, config_delete, config_exists, config_get,
config_set, download_and_extract_theme, download_ezpp_launcher_update,
download_ezpp_launcher_update_files, encrypt_string, exit, extract_theme,
find_osu_installation, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid,
get_launcher_version, get_osu_release_stream, get_osu_skin, get_osu_version, get_platform,
get_skins, get_skins_count, has_net8, has_osuwinello, has_wmctrl,
install_ezpp_launcher_update, is_osu_running, open_url_in_browser, opened_urls,
presence_connect, presence_disconnect, presence_is_connected, presence_update_button,
presence_update_status, presence_update_user, read_theme_info, replace_ui_files, run_osu,
run_osu_updater, set_osu_config_values, set_osu_user_config_values, valid_osu_folder,
},
state::AppState,
};
struct OpenedUrls(Mutex<Vec<PathBuf>>);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
#[cfg(target_os = "linux")]
@@ -59,26 +64,39 @@ pub fn run() {
}
let app = builder
.manage(OpenedUrls(Mutex::new(vec![])))
.setup(|app| {
// ── Config ────────────────────────────────────────────────────────
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".into());
let db_path = PathBuf::from(home).join(".ezpplauncher").join("config.db");
let hwid = get_id().unwrap_or_else(|_| "recorderinsandybridge".into());
let mut cfg = config::Config::new(db_path);
cfg.init(&hwid)?;
// ── Opened files ──────────────────────────────────────────────────
let mut opened_files = Vec::new();
let args = std::env::args().collect::<Vec<_>>();
if args.len() > 1 {
let mut files = Vec::new();
for maybe_file in args.iter().skip(1) {
if maybe_file.starts_with("-") {
continue;
}
let path = PathBuf::from(maybe_file);
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
app.fs_scope().allow_file(&path).ok();
files.push(path);
opened_files.push(path);
}
}
app.state::<OpenedUrls>().0.lock().unwrap().extend(files);
}
app.manage(AppState {
config: Mutex::new(cfg),
opened_files: Mutex::new(opened_files),
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -119,7 +137,13 @@ pub fn run() {
install_ezpp_launcher_update,
read_theme_info,
extract_theme,
download_and_extract_theme
download_and_extract_theme,
config_get,
config_set,
config_exists,
config_delete,
config_clear,
config_all,
])
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
+9
View File
@@ -0,0 +1,9 @@
// state.rs
use crate::config::Config;
use std::path::PathBuf;
use std::sync::Mutex;
pub struct AppState {
pub config: Mutex<Config>,
pub opened_files: Mutex<Vec<PathBuf>>,
}