tracexec_core/cli/
config.rs

1use std::{
2  io,
3  path::PathBuf,
4};
5
6use directories::ProjectDirs;
7use serde::{
8  Deserialize,
9  Deserializer,
10  Serialize,
11};
12use snafu::{
13  ResultExt,
14  Snafu,
15};
16use tracing::warn;
17
18use super::options::{
19  ActivePane,
20  AppLayout,
21  SeccompBpf,
22};
23use crate::timestamp::TimestampFormat;
24
25#[derive(Debug, Default, Clone, Deserialize, Serialize)]
26pub struct Config {
27  pub log: Option<LogModeConfig>,
28  pub tui: Option<TuiModeConfig>,
29  pub modifier: Option<ModifierConfig>,
30  pub ptrace: Option<PtraceConfig>,
31  pub debugger: Option<DebuggerConfig>,
32}
33
34#[derive(Debug, Snafu)]
35pub enum ConfigLoadError {
36  #[snafu(display("Config file not found."))]
37  NotFound,
38  #[snafu(display("Failed to load config file."))]
39  IoError { source: io::Error },
40  #[snafu(display("Failed to parse config file."))]
41  TomlError { source: toml::de::Error },
42}
43
44impl Config {
45  pub fn load(path: Option<PathBuf>) -> Result<Self, ConfigLoadError> {
46    let config_text = match path {
47      Some(path) => std::fs::read_to_string(path).context(IoSnafu)?, // if manually specified config doesn't exist, return a hard error
48      None => {
49        let Some(project_dirs) = project_directory() else {
50          warn!("No valid home directory found! Not loading config.toml.");
51          return Err(ConfigLoadError::NotFound);
52        };
53        // ~/.config/tracexec/config.toml
54        let config_path = project_dirs.config_dir().join("config.toml");
55
56        std::fs::read_to_string(config_path).map_err(|e| match e.kind() {
57          io::ErrorKind::NotFound => ConfigLoadError::NotFound,
58          _ => ConfigLoadError::IoError { source: e },
59        })?
60      }
61    };
62
63    let config: Self = toml::from_str(&config_text).context(TomlSnafu)?;
64    Ok(config)
65  }
66}
67
68#[derive(Debug, Default, Clone, Deserialize, Serialize)]
69pub struct ModifierConfig {
70  pub seccomp_bpf: Option<SeccompBpf>,
71  pub successful_only: Option<bool>,
72  pub fd_in_cmdline: Option<bool>,
73  pub stdio_in_cmdline: Option<bool>,
74  pub resolve_proc_self_exe: Option<bool>,
75  pub hide_cloexec_fds: Option<bool>,
76  pub timestamp: Option<TimestampConfig>,
77}
78
79#[derive(Debug, Default, Clone, Deserialize, Serialize)]
80pub struct TimestampConfig {
81  pub enable: bool,
82  pub inline_format: Option<TimestampFormat>,
83}
84
85#[derive(Debug, Default, Clone, Deserialize, Serialize)]
86pub struct PtraceConfig {
87  pub seccomp_bpf: Option<SeccompBpf>,
88}
89
90#[derive(Debug, Default, Clone, Deserialize, Serialize)]
91pub struct TuiModeConfig {
92  pub follow: Option<bool>,
93  pub exit_handling: Option<ExitHandling>,
94  pub active_pane: Option<ActivePane>,
95  pub layout: Option<AppLayout>,
96  #[serde(default, deserialize_with = "deserialize_frame_rate")]
97  pub frame_rate: Option<f64>,
98  pub max_events: Option<u64>,
99}
100
101#[derive(Debug, Default, Clone, Deserialize, Serialize)]
102pub struct DebuggerConfig {
103  pub default_external_command: Option<String>,
104}
105
106fn is_frame_rate_invalid(v: f64) -> bool {
107  v.is_nan() || v <= 0. || v.is_infinite()
108}
109
110fn deserialize_frame_rate<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
111where
112  D: Deserializer<'de>,
113{
114  let value = Option::<f64>::deserialize(deserializer)?;
115  if value.is_some_and(is_frame_rate_invalid) {
116    return Err(serde::de::Error::invalid_value(
117      serde::de::Unexpected::Float(value.unwrap()),
118      &"a positive floating-point number",
119    ));
120  }
121  Ok(value)
122}
123
124#[derive(Debug, Default, Clone, Deserialize, Serialize)]
125pub struct LogModeConfig {
126  pub show_interpreter: Option<bool>,
127  pub color_level: Option<ColorLevel>,
128  pub foreground: Option<bool>,
129  pub fd_display: Option<FileDescriptorDisplay>,
130  pub env_display: Option<EnvDisplay>,
131  pub show_comm: Option<bool>,
132  pub show_argv: Option<bool>,
133  pub show_filename: Option<bool>,
134  pub show_cwd: Option<bool>,
135  pub show_cmdline: Option<bool>,
136  pub decode_errno: Option<bool>,
137}
138
139#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
140pub enum ColorLevel {
141  Less,
142  #[default]
143  Normal,
144  More,
145}
146
147#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
148pub enum FileDescriptorDisplay {
149  Hide,
150  Show,
151  #[default]
152  Diff,
153}
154
155#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
156pub enum EnvDisplay {
157  Hide,
158  Show,
159  #[default]
160  Diff,
161}
162
163#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
164pub enum ExitHandling {
165  #[default]
166  Wait,
167  Kill,
168  Terminate,
169}
170
171pub fn project_directory() -> Option<ProjectDirs> {
172  ProjectDirs::from("dev", "kxxt", env!("CARGO_PKG_NAME"))
173}
174
175#[cfg(test)]
176mod tests {
177  use std::path::PathBuf;
178
179  use toml;
180
181  use super::*;
182
183  #[test]
184  fn test_validate_frame_rate() {
185    // valid frame rates
186    assert!(!is_frame_rate_invalid(5.0));
187    assert!(!is_frame_rate_invalid(12.5));
188
189    // too low or zero
190    assert!(is_frame_rate_invalid(0.0));
191    assert!(is_frame_rate_invalid(-1.0));
192
193    // NaN or infinite
194    assert!(is_frame_rate_invalid(f64::NAN));
195    assert!(is_frame_rate_invalid(f64::INFINITY));
196    assert!(is_frame_rate_invalid(f64::NEG_INFINITY));
197  }
198
199  #[derive(Serialize, Deserialize)]
200  struct FrameRate {
201    #[serde(default, deserialize_with = "deserialize_frame_rate")]
202    frame_rate: Option<f64>,
203  }
204
205  #[test]
206  fn test_deserialize_frame_rate_valid() {
207    let value: FrameRate = toml::from_str("frame_rate = 12.5").unwrap();
208    assert_eq!(value.frame_rate, Some(12.5));
209
210    let value: FrameRate = toml::from_str("frame_rate = 5.0").unwrap();
211    assert_eq!(value.frame_rate, Some(5.0));
212  }
213
214  #[test]
215  fn test_deserialize_frame_rate_invalid() {
216    let value: Result<FrameRate, _> = toml::from_str("frame_rate = -1");
217    assert!(value.is_err());
218
219    let value: Result<FrameRate, _> = toml::from_str("frame_rate = NaN");
220    assert!(value.is_err());
221
222    let value: Result<FrameRate, _> = toml::from_str("frame_rate = 0");
223    assert!(value.is_err());
224  }
225
226  #[test]
227  fn test_config_load_invalid_path() {
228    let path = Some(PathBuf::from("/non/existent/config.toml"));
229    let result = Config::load(path);
230    assert!(matches!(
231      result,
232      Err(ConfigLoadError::IoError { .. }) | Err(ConfigLoadError::NotFound)
233    ));
234  }
235
236  #[test]
237  fn test_modifier_config_roundtrip() {
238    let toml_str = r#"
239seccomp_bpf = "Auto"
240successful_only = true
241fd_in_cmdline = false
242stdio_in_cmdline = true
243resolve_proc_self_exe = true
244hide_cloexec_fds = false
245
246[timestamp]
247enable = true
248inline_format = "%H:%M:%S"
249"#;
250
251    let cfg: ModifierConfig = toml::from_str(toml_str).unwrap();
252    assert_eq!(cfg.successful_only.unwrap(), true);
253    assert_eq!(cfg.stdio_in_cmdline.unwrap(), true);
254    assert_eq!(cfg.timestamp.as_ref().unwrap().enable, true);
255    assert_eq!(
256      cfg
257        .timestamp
258        .as_ref()
259        .unwrap()
260        .inline_format
261        .as_ref()
262        .unwrap()
263        .as_str(),
264      "%H:%M:%S"
265    );
266  }
267
268  #[test]
269  fn test_ptrace_config_roundtrip() {
270    let toml_str = r#"seccomp_bpf = "Auto""#;
271    let cfg: PtraceConfig = toml::from_str(toml_str).unwrap();
272    assert_eq!(cfg.seccomp_bpf.unwrap(), SeccompBpf::Auto);
273  }
274
275  #[test]
276  fn test_log_mode_config_roundtrip() {
277    let toml_str = r#"
278show_interpreter = true
279color_level = "More"
280foreground = false
281"#;
282    let cfg: LogModeConfig = toml::from_str(toml_str).unwrap();
283    assert_eq!(cfg.show_interpreter.unwrap(), true);
284    assert_eq!(cfg.color_level.unwrap(), ColorLevel::More);
285    assert_eq!(cfg.foreground.unwrap(), false);
286  }
287
288  #[test]
289  fn test_tui_mode_config_roundtrip() {
290    let toml_str = r#"
291follow = true
292frame_rate = 12.5
293max_events = 100
294"#;
295    let cfg: TuiModeConfig = toml::from_str(toml_str).unwrap();
296    assert_eq!(cfg.follow.unwrap(), true);
297    assert_eq!(cfg.frame_rate.unwrap(), 12.5);
298    assert_eq!(cfg.max_events.unwrap(), 100);
299  }
300
301  #[test]
302  fn test_debugger_config_roundtrip() {
303    let toml_str = r#"default_external_command = "echo hello""#;
304    let cfg: DebuggerConfig = toml::from_str(toml_str).unwrap();
305    assert_eq!(cfg.default_external_command.unwrap(), "echo hello");
306  }
307}