chore: replace json config with database
This commit is contained in:
1 parent
2ba094f07e
commit
20e8389409
16 files changed
+761
-226
No files matched your search
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
Reference in new issue
Block a user