1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Unified configuration file — `~/.config/blazehash/config.toml`
//!
//! All subsystem configs live under named TOML tables:
//!
//! ```toml
//! [parallel]
//! parallel_threshold_bytes = 4096
//! calibrated_at = "2026-04-10"
//! cpu_info = "Apple M4 Pro"
//!
//! [gpu]
//! device = "Apple M4 Pro GPU"
//! calibrated = "2026-04-10"
//! threshold_single_mb = 256
//! threshold_multi_mb = 32
//! gpu_enabled = true
//! ```
use crate::parallel_config::ParallelConfig;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[cfg(feature = "gpu")]
use crate::gpu::config::GpuConfig;
/// User-facing defaults read from `blazehash.toml`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigDefaults {
/// Default hash algorithms when `-c` is not specified.
#[serde(default = "default_algorithms")]
pub algorithms: Vec<String>,
/// Default output format (hashdeep, json, jsonl, csv, etc.)
#[serde(default)]
pub output_format: Option<String>,
/// Default signing key path.
#[serde(default)]
pub sign_key_path: Option<String>,
/// Default case ID for manifest headers.
#[serde(default)]
pub case_id: Option<String>,
/// Default examiner name for manifest headers.
#[serde(default)]
pub examiner: Option<String>,
}
fn default_algorithms() -> Vec<String> {
vec!["blake3".to_string()]
}
impl Default for ConfigDefaults {
fn default() -> Self {
Self {
algorithms: default_algorithms(),
output_format: None,
sign_key_path: None,
case_id: None,
examiner: None,
}
}
}
/// Top-level config written to / read from `config.toml`.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BlazeConfig {
/// User-facing defaults (read from `blazehash.toml`).
#[serde(default)]
pub defaults: ConfigDefaults,
/// Parallel threshold calibration — written by `blazehash bench`.
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel: Option<ParallelConfig>,
/// GPU calibration — written by `blazehash bench --gpu`.
#[cfg(feature = "gpu")]
#[serde(skip_serializing_if = "Option::is_none")]
pub gpu: Option<GpuConfig>,
}
impl BlazeConfig {
/// Load from `path`. Returns `Default` if file is absent or unparseable.
pub fn load(path: &Path) -> Self {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return Self::default(),
};
toml::from_str(&content).unwrap_or_default()
}
/// Load from the canonical config path (`$BLAZEHASH_CONFIG_DIR/config.toml`).
pub fn load_default() -> Self {
Self::load(&config_path())
}
/// Write to `path`, creating parent directories as needed.
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string(self)?;
std::fs::write(path, content)?;
Ok(())
}
/// Save to the canonical config path.
pub fn save_default(&self) -> Result<()> {
self.save(&config_path())
}
}
/// Canonical path for the unified config file.
///
/// Respects `$BLAZEHASH_CONFIG_DIR`; falls back to `~/.config/blazehash/`.
pub fn config_path() -> PathBuf {
config_dir().join("config.toml")
}
/// Directory that holds blazehash config files.
pub fn config_dir() -> PathBuf {
if let Ok(dir) = std::env::var("BLAZEHASH_CONFIG_DIR") {
return PathBuf::from(dir);
}
if let Some(cfg) = dirs::config_dir() {
cfg.join("blazehash")
} else {
PathBuf::from(".")
}
}
/// Load user-facing config from `blazehash.toml`.
///
/// Search order:
/// 1. `./blazehash.toml` (current working directory)
/// 2. `~/.config/blazehash/blazehash.toml` (XDG config dir)
///
/// Returns `BlazeConfig::default()` if neither file exists or can be parsed.
pub fn load_user_config() -> BlazeConfig {
// Try current directory first
if let Ok(s) = std::fs::read_to_string("blazehash.toml") {
if let Ok(cfg) = toml::from_str::<BlazeConfig>(&s) {
return cfg;
}
}
// Fall back to XDG config dir
let global = config_dir().join("blazehash.toml");
if let Ok(s) = std::fs::read_to_string(&global) {
if let Ok(cfg) = toml::from_str::<BlazeConfig>(&s) {
return cfg;
}
}
BlazeConfig::default()
}