Skip to main content

tracexec_core/cli/
config.rs

1use std::{
2  io,
3  path::{
4    Path,
5    PathBuf,
6  },
7  sync::OnceLock,
8};
9
10use directories::ProjectDirs;
11use serde::{
12  Deserialize,
13  Deserializer,
14  Serialize,
15};
16use snafu::{
17  ResultExt,
18  Snafu,
19};
20use tracing::warn;
21
22use super::options::{
23  ActivePane,
24  AppLayout,
25  SeccompBpf,
26};
27use crate::{
28  cli::keys::TuiKeyBindingsConfig,
29  timestamp::TimestampFormat,
30};
31
32/// Wrapper around `ProjectDirs` that supports overriding directories
33/// (e.g. when running elevated via sudo, to use the original user's dirs).
34#[derive(Debug, Clone)]
35pub struct TracexecProjectDirs {
36  config_dir: PathBuf,
37  data_dir: PathBuf,
38  data_local_dir: PathBuf,
39}
40
41impl TracexecProjectDirs {
42  pub fn config_dir(&self) -> &Path {
43    &self.config_dir
44  }
45
46  pub fn data_dir(&self) -> &Path {
47    &self.data_dir
48  }
49
50  pub fn data_local_dir(&self) -> &Path {
51    &self.data_local_dir
52  }
53}
54
55impl From<ProjectDirs> for TracexecProjectDirs {
56  fn from(dirs: ProjectDirs) -> Self {
57    Self {
58      config_dir: dirs.config_dir().to_path_buf(),
59      data_dir: dirs.data_dir().to_path_buf(),
60      data_local_dir: dirs.data_local_dir().to_path_buf(),
61    }
62  }
63}
64
65struct ProjectDirOverrides {
66  config_dir: PathBuf,
67  data_dir: PathBuf,
68  data_local_dir: PathBuf,
69}
70
71static PROJECT_DIR_OVERRIDES: OnceLock<ProjectDirOverrides> = OnceLock::new();
72
73/// Set overrides for the project directories. Must be called before any call to
74/// `project_directory()`. Used by the elevated process to point at the original
75/// user's config/data directories.
76pub fn set_project_dir_overrides(config_dir: PathBuf, data_dir: PathBuf, data_local_dir: PathBuf) {
77  PROJECT_DIR_OVERRIDES
78    .set(ProjectDirOverrides {
79      config_dir,
80      data_dir,
81      data_local_dir,
82    })
83    .ok();
84}
85
86#[derive(Debug, Default, Clone, Deserialize, Serialize)]
87pub struct Config {
88  pub log: Option<LogModeConfig>,
89  pub tui: Option<TuiModeConfig>,
90  pub modifier: Option<ModifierConfig>,
91  pub ptrace: Option<PtraceConfig>,
92  pub debugger: Option<DebuggerConfig>,
93}
94
95#[derive(Debug, Snafu)]
96pub enum ConfigLoadError {
97  #[snafu(display("Config file not found."))]
98  NotFound,
99  #[snafu(display("Failed to load config file."))]
100  IoError { source: io::Error },
101  #[snafu(display("Failed to parse config file."))]
102  TomlError { source: toml::de::Error },
103}
104
105impl Config {
106  pub fn load(path: Option<PathBuf>) -> Result<Self, ConfigLoadError> {
107    let config_text = match path {
108      Some(path) => std::fs::read_to_string(path).context(IoSnafu)?, // if manually specified config doesn't exist, return a hard error
109      None => {
110        let Some(project_dirs) = project_directory() else {
111          warn!("No valid home directory found! Not loading config.toml.");
112          return Err(ConfigLoadError::NotFound);
113        };
114        // ~/.config/tracexec/config.toml
115        let config_path = project_dirs.config_dir().join("config.toml");
116
117        std::fs::read_to_string(config_path).map_err(|e| match e.kind() {
118          io::ErrorKind::NotFound => ConfigLoadError::NotFound,
119          _ => ConfigLoadError::IoError { source: e },
120        })?
121      }
122    };
123
124    let config: Self = toml::from_str(&config_text).context(TomlSnafu)?;
125    Ok(config)
126  }
127}
128
129#[derive(Debug, Default, Clone, Deserialize, Serialize)]
130pub struct ModifierConfig {
131  pub seccomp_bpf: Option<SeccompBpf>,
132  pub successful_only: Option<bool>,
133  pub fd_in_cmdline: Option<bool>,
134  pub stdio_in_cmdline: Option<bool>,
135  pub resolve_proc_self_exe: Option<bool>,
136  pub hide_cloexec_fds: Option<bool>,
137  pub timestamp: Option<TimestampConfig>,
138  pub collect_cgroup: Option<bool>,
139}
140
141#[derive(Debug, Default, Clone, Deserialize, Serialize)]
142pub struct TimestampConfig {
143  pub enable: bool,
144  pub inline_format: Option<TimestampFormat>,
145}
146
147#[derive(Debug, Default, Clone, Deserialize, Serialize)]
148pub struct PtraceConfig {
149  pub seccomp_bpf: Option<SeccompBpf>,
150}
151
152#[derive(Debug, Default, Clone, Deserialize, Serialize)]
153pub struct TuiModeConfig {
154  pub follow: Option<bool>,
155  pub exit_handling: Option<ExitHandling>,
156  pub active_pane: Option<ActivePane>,
157  pub layout: Option<AppLayout>,
158  #[serde(default, deserialize_with = "deserialize_frame_rate")]
159  pub frame_rate: Option<f64>,
160  pub max_events: Option<u64>,
161  pub scrollback_lines: Option<usize>,
162  #[serde(rename = "theme-file")]
163  pub theme_file: Option<PathBuf>,
164  #[serde(default)]
165  pub theme: Option<crate::cli::tui_theme::ThemeSpec>,
166  #[serde(default)]
167  pub keys: Option<TuiKeyBindingsConfig>,
168}
169
170#[derive(Debug, Default, Clone, Deserialize, Serialize)]
171pub struct DebuggerConfig {
172  pub default_external_command: Option<String>,
173}
174
175fn is_frame_rate_invalid(v: f64) -> bool {
176  v.is_nan() || v <= 0. || v.is_infinite()
177}
178
179fn deserialize_frame_rate<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
180where
181  D: Deserializer<'de>,
182{
183  let value = Option::<f64>::deserialize(deserializer)?;
184  if let Some(value) = value.filter(|value| is_frame_rate_invalid(*value)) {
185    return Err(serde::de::Error::invalid_value(
186      serde::de::Unexpected::Float(value),
187      &"a positive floating-point number",
188    ));
189  }
190  Ok(value)
191}
192
193#[derive(Debug, Default, Clone, Deserialize, Serialize)]
194pub struct LogModeConfig {
195  pub show_interpreter: Option<bool>,
196  pub color_level: Option<ColorLevel>,
197  pub foreground: Option<bool>,
198  pub fd_display: Option<FileDescriptorDisplay>,
199  pub env_display: Option<EnvDisplay>,
200  pub show_comm: Option<bool>,
201  pub show_argv: Option<bool>,
202  pub show_filename: Option<bool>,
203  pub show_cwd: Option<bool>,
204  pub show_cmdline: Option<bool>,
205  pub decode_errno: Option<bool>,
206}
207
208#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
209pub enum ColorLevel {
210  Less,
211  #[default]
212  Normal,
213  More,
214}
215
216#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
217pub enum FileDescriptorDisplay {
218  Hide,
219  Show,
220  #[default]
221  Diff,
222}
223
224#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
225pub enum EnvDisplay {
226  Hide,
227  Show,
228  #[default]
229  Diff,
230}
231
232#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
233pub enum ExitHandling {
234  #[default]
235  Wait,
236  Kill,
237  Terminate,
238}
239
240pub fn project_directory() -> Option<TracexecProjectDirs> {
241  if let Some(overrides) = PROJECT_DIR_OVERRIDES.get() {
242    return Some(TracexecProjectDirs {
243      config_dir: overrides.config_dir.clone(),
244      // On Linux data_dir and data_local_dir are the same for ProjectDirs.
245      data_dir: overrides.data_dir.clone(),
246      data_local_dir: overrides.data_local_dir.clone(),
247    });
248  }
249  ProjectDirs::from("dev", "kxxt", "tracexec").map(TracexecProjectDirs::from)
250}
251
252#[cfg(test)]
253mod tests {
254  use std::path::PathBuf;
255
256  use test_that::prelude::*;
257  use toml;
258
259  use super::*;
260
261  #[test]
262  fn test_validate_frame_rate() {
263    // valid frame rates
264    assert!(!is_frame_rate_invalid(5.0));
265    assert!(!is_frame_rate_invalid(12.5));
266
267    // too low or zero
268    assert!(is_frame_rate_invalid(0.0));
269    assert!(is_frame_rate_invalid(-1.0));
270
271    // NaN or infinite
272    assert!(is_frame_rate_invalid(f64::NAN));
273    assert!(is_frame_rate_invalid(f64::INFINITY));
274    assert!(is_frame_rate_invalid(f64::NEG_INFINITY));
275  }
276
277  #[derive(Serialize, Deserialize)]
278  struct FrameRate {
279    #[serde(default, deserialize_with = "deserialize_frame_rate")]
280    frame_rate: Option<f64>,
281  }
282
283  #[test]
284  fn test_deserialize_frame_rate_valid() {
285    let value: FrameRate = toml::from_str("frame_rate = 12.5").unwrap();
286    assert_eq!(value.frame_rate, Some(12.5));
287
288    let value: FrameRate = toml::from_str("frame_rate = 5.0").unwrap();
289    assert_eq!(value.frame_rate, Some(5.0));
290  }
291
292  #[test]
293  fn test_deserialize_frame_rate_invalid() {
294    let value: Result<FrameRate, _> = toml::from_str("frame_rate = -1");
295    assert_that!(value.err(), some(anything()));
296
297    let value: Result<FrameRate, _> = toml::from_str("frame_rate = NaN");
298    assert_that!(value.err(), some(anything()));
299
300    let value: Result<FrameRate, _> = toml::from_str("frame_rate = 0");
301    assert_that!(value.err(), some(anything()));
302  }
303
304  #[test]
305  fn test_config_load_invalid_path() {
306    let path = Some(PathBuf::from("/non/existent/config.toml"));
307    let result = Config::load(path);
308    assert!(matches!(
309      result,
310      Err(ConfigLoadError::IoError { .. }) | Err(ConfigLoadError::NotFound)
311    ));
312  }
313
314  #[test]
315  fn test_modifier_config_roundtrip() {
316    let toml_str = r#"
317seccomp_bpf = "Auto"
318successful_only = true
319fd_in_cmdline = false
320stdio_in_cmdline = true
321resolve_proc_self_exe = true
322hide_cloexec_fds = false
323
324[timestamp]
325enable = true
326inline_format = "%H:%M:%S"
327"#;
328
329    let cfg: ModifierConfig = toml::from_str(toml_str).unwrap();
330    assert!(cfg.successful_only.unwrap());
331    assert!(cfg.stdio_in_cmdline.unwrap());
332    assert!(cfg.timestamp.as_ref().unwrap().enable);
333    assert_eq!(
334      cfg
335        .timestamp
336        .as_ref()
337        .unwrap()
338        .inline_format
339        .as_ref()
340        .unwrap()
341        .as_str(),
342      "%H:%M:%S"
343    );
344  }
345
346  #[test]
347  fn test_ptrace_config_roundtrip() {
348    let toml_str = r#"seccomp_bpf = "Auto""#;
349    let cfg: PtraceConfig = toml::from_str(toml_str).unwrap();
350    assert_eq!(cfg.seccomp_bpf.unwrap(), SeccompBpf::Auto);
351  }
352
353  #[test]
354  fn test_log_mode_config_roundtrip() {
355    let toml_str = r#"
356show_interpreter = true
357color_level = "More"
358foreground = false
359"#;
360    let cfg: LogModeConfig = toml::from_str(toml_str).unwrap();
361    assert!(cfg.show_interpreter.unwrap());
362    assert_eq!(cfg.color_level.unwrap(), ColorLevel::More);
363    assert!(!cfg.foreground.unwrap());
364  }
365
366  #[test]
367  fn test_tui_mode_config_roundtrip() {
368    let toml_str = r#"
369follow = true
370frame_rate = 12.5
371max_events = 100
372theme-file = "nord.toml"
373theme = { app-title = { fg = "cyan", modifiers = ["bold"] } }
374"#;
375    let cfg: TuiModeConfig = toml::from_str(toml_str).unwrap();
376    assert!(cfg.follow.unwrap());
377    assert_eq!(cfg.frame_rate.unwrap(), 12.5);
378    assert_eq!(cfg.max_events.unwrap(), 100);
379    assert_eq!(cfg.theme_file, Some(PathBuf::from("nord.toml")));
380    let theme = cfg.theme.unwrap();
381    assert_that!(theme.app_title, some(anything()));
382    let app_title = theme.app_title.unwrap();
383    assert!(
384      matches!(app_title.fg, Some(crate::cli::tui_theme::ThemeColor::Named(ref s)) if s == "cyan")
385    );
386    assert_eq!(app_title.modifiers.len(), 1);
387  }
388
389  #[test]
390  fn test_debugger_config_roundtrip() {
391    let toml_str = r#"default_external_command = "echo hello""#;
392    let cfg: DebuggerConfig = toml::from_str(toml_str).unwrap();
393    assert_eq!(cfg.default_external_command.unwrap(), "echo hello");
394  }
395}