Version 1.0.0
This commit is contained in:
+331
@@ -0,0 +1,331 @@
|
||||
//! Storage management for configuration and exports.
|
||||
//!
|
||||
//! All application data lives under `~/.config/KAT/`:
|
||||
//!
|
||||
//! ```text
|
||||
//! ~/.config/KAT/
|
||||
//! config.ini — User configuration
|
||||
//! exports/ — Exported .fob / .sub files
|
||||
//! ```
|
||||
//!
|
||||
//! Captures are **in-memory only** and are discarded when KAT exits.
|
||||
//! Only explicitly exported signals (.fob / .sub) persist between runs.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use configparser::ini::Ini;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Application configuration loaded from `~/.config/KAT/config.ini`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
// [general]
|
||||
/// Directory for exporting signals (.fob / .sub files)
|
||||
pub export_directory: PathBuf,
|
||||
/// Maximum captures to keep in memory during a session
|
||||
pub max_captures: usize,
|
||||
|
||||
// [radio]
|
||||
/// Default frequency in Hz
|
||||
pub default_frequency: u32,
|
||||
/// Default LNA gain (0-40 dB, 8 dB steps)
|
||||
pub default_lna_gain: u32,
|
||||
/// Default VGA gain (0-62 dB, 2 dB steps)
|
||||
pub default_vga_gain: u32,
|
||||
/// Default amplifier state
|
||||
pub default_amp: bool,
|
||||
|
||||
// [export]
|
||||
/// Default export format (fob or sub)
|
||||
pub default_export_format: String,
|
||||
/// Include raw level/duration pairs in exports
|
||||
pub include_raw_pairs: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Build the default config, using the given config_dir as the base.
|
||||
/// This keeps everything under `~/.config/KAT/` by default.
|
||||
fn default_for(config_dir: &PathBuf) -> Self {
|
||||
Self {
|
||||
export_directory: config_dir.join("exports"),
|
||||
max_captures: 100,
|
||||
default_frequency: 433_920_000,
|
||||
default_lna_gain: 24,
|
||||
default_vga_gain: 20,
|
||||
default_amp: false,
|
||||
default_export_format: "fob".to_string(),
|
||||
include_raw_pairs: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load config from an INI file, falling back to defaults for missing keys.
|
||||
fn load_from_ini(path: &std::path::Path, config_dir: &PathBuf) -> Result<Self> {
|
||||
let mut ini = Ini::new();
|
||||
ini.load(path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
|
||||
|
||||
let defaults = Config::default_for(config_dir);
|
||||
|
||||
let export_directory = ini
|
||||
.get("general", "export_directory")
|
||||
.map(|s| expand_tilde(&s))
|
||||
.unwrap_or(defaults.export_directory);
|
||||
|
||||
let max_captures = ini
|
||||
.getuint("general", "max_captures")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(defaults.max_captures);
|
||||
|
||||
let default_frequency = ini
|
||||
.getuint("radio", "default_frequency")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(defaults.default_frequency);
|
||||
|
||||
let default_lna_gain = ini
|
||||
.getuint("radio", "default_lna_gain")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(defaults.default_lna_gain);
|
||||
|
||||
let default_vga_gain = ini
|
||||
.getuint("radio", "default_vga_gain")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(defaults.default_vga_gain);
|
||||
|
||||
let default_amp = ini
|
||||
.getbool("radio", "default_amp")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(defaults.default_amp);
|
||||
|
||||
let default_export_format = ini
|
||||
.get("export", "default_format")
|
||||
.unwrap_or(defaults.default_export_format);
|
||||
|
||||
let include_raw_pairs = ini
|
||||
.getbool("export", "include_raw_pairs")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(defaults.include_raw_pairs);
|
||||
|
||||
Ok(Self {
|
||||
export_directory,
|
||||
max_captures,
|
||||
default_frequency,
|
||||
default_lna_gain,
|
||||
default_vga_gain,
|
||||
default_amp,
|
||||
default_export_format,
|
||||
include_raw_pairs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save config to an INI-style file with comments explaining each field.
|
||||
fn save_to_ini(&self, path: &std::path::Path) -> Result<()> {
|
||||
let export_str = self.export_directory.to_string_lossy();
|
||||
let freq_mhz = self.default_frequency as f64 / 1_000_000.0;
|
||||
|
||||
let content = format!(
|
||||
r#"; KAT — Keyfob Analysis Toolkit configuration
|
||||
; Location: {path}
|
||||
;
|
||||
; Edit this file to change default settings.
|
||||
; Lines starting with ; or # are comments.
|
||||
|
||||
[general]
|
||||
; Directory where .fob and .sub exports are saved.
|
||||
; Supports ~ for home directory.
|
||||
export_directory = {export_dir}
|
||||
|
||||
; Maximum number of captures to keep in memory per session.
|
||||
; Captures are NOT persisted between runs — only exported
|
||||
; signals (.fob / .sub) survive in the exports folder.
|
||||
max_captures = {max_captures}
|
||||
|
||||
[radio]
|
||||
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
|
||||
; Common keyfob frequencies: 315000000, 433920000, 868350000
|
||||
default_frequency = {frequency}
|
||||
|
||||
; Default LNA gain in dB (0, 8, 16, 24, 32, 40)
|
||||
default_lna_gain = {lna}
|
||||
|
||||
; Default VGA gain in dB (0-62, even numbers)
|
||||
default_vga_gain = {vga}
|
||||
|
||||
; Enable RF amplifier by default (true/false)
|
||||
default_amp = {amp}
|
||||
|
||||
[export]
|
||||
; Default export format: fob (JSON metadata) or sub (Flipper Zero)
|
||||
default_format = {export_fmt}
|
||||
|
||||
; Include raw signal level/duration pairs in .fob exports.
|
||||
; Enables signal replay but increases file size.
|
||||
include_raw_pairs = {raw_pairs}
|
||||
"#,
|
||||
path = path.display(),
|
||||
export_dir = export_str,
|
||||
max_captures = self.max_captures,
|
||||
freq_mhz = freq_mhz,
|
||||
frequency = self.default_frequency,
|
||||
lna = self.default_lna_gain,
|
||||
vga = self.default_vga_gain,
|
||||
amp = self.default_amp,
|
||||
export_fmt = self.default_export_format,
|
||||
raw_pairs = self.include_raw_pairs,
|
||||
);
|
||||
|
||||
fs::write(path, content)
|
||||
.with_context(|| format!("Failed to write config to {:?}", path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback Default (without knowing config_dir). Only used if something goes
|
||||
/// very wrong and we need a Config without a Storage instance.
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let fallback = resolve_config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from(".").join("KAT"));
|
||||
Config::default_for(&fallback)
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand `~` at the start of a path to the user's home directory.
|
||||
fn expand_tilde(s: &str) -> PathBuf {
|
||||
if s.starts_with("~/") {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(&s[2..]);
|
||||
}
|
||||
}
|
||||
PathBuf::from(s)
|
||||
}
|
||||
|
||||
/// Resolve the KAT config directory to `~/.config/KAT/` regardless of OS.
|
||||
pub fn resolve_config_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".config").join("KAT"))
|
||||
}
|
||||
|
||||
// ─── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Storage manager for configuration and exports.
|
||||
///
|
||||
/// On construction it ensures the directory tree exists:
|
||||
///
|
||||
/// ```text
|
||||
/// ~/.config/KAT/
|
||||
/// config.ini
|
||||
/// exports/
|
||||
/// ```
|
||||
///
|
||||
/// Captures are in-memory only — they are discarded on exit.
|
||||
pub struct Storage {
|
||||
/// Base config directory (~/.config/KAT)
|
||||
config_dir: PathBuf,
|
||||
/// Configuration
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
/// Create a new storage manager.
|
||||
///
|
||||
/// 1. Resolves the config directory (`~/.config/KAT`).
|
||||
/// 2. Creates it if missing.
|
||||
/// 3. Loads `config.ini` if it exists, otherwise writes a default one.
|
||||
/// 4. Creates the export directory if missing.
|
||||
pub fn new() -> Result<Self> {
|
||||
// ── 1. Resolve base path ─────────────────────────────────────────
|
||||
let config_dir = resolve_config_dir()
|
||||
.context("Could not determine home directory (is $HOME set?)")?;
|
||||
|
||||
let config_path = config_dir.join("config.ini");
|
||||
|
||||
// ── 2. Ensure directory tree exists ──────────────────────────────
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)
|
||||
.with_context(|| format!("Failed to create config dir: {:?}", config_dir))?;
|
||||
tracing::info!("Created config directory: {:?}", config_dir);
|
||||
}
|
||||
|
||||
// ── 3. Load or create config.ini ─────────────────────────────────
|
||||
let config = if config_path.exists() {
|
||||
tracing::info!("Loading config from {:?}", config_path);
|
||||
match Config::load_from_ini(&config_path, &config_dir) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to parse config.ini, using defaults: {}",
|
||||
e
|
||||
);
|
||||
Config::default_for(&config_dir)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::info!(
|
||||
"No config.ini found — creating default at {:?}",
|
||||
config_path
|
||||
);
|
||||
let config = Config::default_for(&config_dir);
|
||||
if let Err(e) = config.save_to_ini(&config_path) {
|
||||
tracing::warn!("Could not write default config.ini: {}", e);
|
||||
}
|
||||
config
|
||||
};
|
||||
|
||||
// ── 4. Ensure export directory exists ────────────────────────────
|
||||
if !config.export_directory.exists() {
|
||||
fs::create_dir_all(&config.export_directory).with_context(|| {
|
||||
format!(
|
||||
"Failed to create export dir: {:?}",
|
||||
config.export_directory
|
||||
)
|
||||
})?;
|
||||
tracing::info!(
|
||||
"Created export directory: {:?}",
|
||||
config.export_directory
|
||||
);
|
||||
}
|
||||
|
||||
// ── 5. Log resolved paths ───────────────────────────────────────
|
||||
tracing::info!("Config dir: {:?}", config_dir);
|
||||
tracing::info!("Export dir: {:?}", config.export_directory);
|
||||
|
||||
Ok(Self {
|
||||
config_dir,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save the current configuration back to `config.ini`.
|
||||
#[allow(dead_code)]
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
let config_path = self.config_dir.join("config.ini");
|
||||
self.config.save_to_ini(&config_path)?;
|
||||
tracing::info!("Saved config to {:?}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ─── Path accessors ──────────────────────────────────────────────────
|
||||
|
||||
/// Get the config directory path (`~/.config/KAT`)
|
||||
#[allow(dead_code)]
|
||||
pub fn config_dir(&self) -> &PathBuf {
|
||||
&self.config_dir
|
||||
}
|
||||
|
||||
/// Get the export directory path (from config, default `~/.config/KAT/exports`)
|
||||
pub fn export_dir(&self) -> &PathBuf {
|
||||
&self.config.export_directory
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user