1use std::path::PathBuf;
15
16use anyhow::{Result, bail};
17use serde::{Deserialize, Serialize};
18
19pub const OUTPUT_VERSION: u32 = 2;
21pub const DEFAULT_ID: &str = "default";
22pub const ADMIN_PATH: &str = "/etc/scx/catalog.json";
23pub const SHIPPED_PATH: &str = "/usr/share/scx/catalog.json";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Pick {
28 pub id: String,
29 pub label: String,
30 #[serde(default)]
31 pub description: String,
32 #[serde(default)]
33 pub path: Option<String>,
34 pub available: bool,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct Installed {
40 pub name: String,
41 pub path: String,
42 pub recommended: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct Unit {
47 pub active: bool,
48 pub enabled: bool,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Running {
53 pub sched: String,
54 pub args: Vec<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Next {
59 pub sched: String,
60 pub args: Vec<String>,
61 #[serde(rename = "override")]
62 pub overridden: bool,
63}
64
65impl Next {
66 pub fn name(&self) -> &str {
68 self.sched.rsplit('/').next().unwrap_or(&self.sched)
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct Kernel {
74 pub state: String,
75 pub ops: Option<String>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct Failure {
80 pub message: String,
81 pub status: String,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct Current {
87 pub version: u32,
88 pub scheduler: Option<String>,
91 pub unit: Unit,
92 pub running: Option<Running>,
93 pub next: Option<Next>,
94 pub kernel: Option<Kernel>,
95 pub failure: Option<Failure>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub config_error: Option<String>,
98}
99
100impl Current {
101 pub fn attached(&self) -> bool {
102 self.kernel.as_ref().is_some_and(|k| k.ops.is_some())
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct Listing {
109 pub version: u32,
110 pub source: PathBuf,
111 pub customized: bool,
112 pub recommended: Vec<Pick>,
113 #[serde(default)]
114 pub all: Vec<Installed>,
115 #[serde(default)]
116 pub config: Option<PathBuf>,
117 pub current: Current,
118}
119
120pub fn validate_arg(arg: &str) -> Result<()> {
122 let ok = (1..=256).contains(&arg.len())
123 && arg
124 .chars()
125 .all(|c| c.is_ascii_alphanumeric() || "-_=.,:/+".contains(c));
126 if !ok {
127 bail!("invalid option {arg:?}: ASCII letters, digits and -_=.,:/+ only");
128 }
129 Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134 use std::fs;
135 use std::path::Path;
136
137 use super::*;
138
139 fn fixtures() -> PathBuf {
140 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../rust/scx_catalog/fixtures")
141 }
142
143 #[test]
147 fn the_shared_fixtures_round_trip_through_these_types() {
148 let mut seen = 0;
149 for entry in fs::read_dir(fixtures().join("detection")).unwrap() {
150 let path = entry.unwrap().path();
151 if path.extension().is_none_or(|e| e != "json") {
152 continue;
153 }
154 let text = fs::read_to_string(&path).unwrap();
155 let case: serde_json::Value = serde_json::from_str(&text).unwrap();
156 let value = case["expected"].clone();
157 let current: Current = serde_json::from_value(value.clone())
158 .unwrap_or_else(|err| panic!("{}: {err}", path.display()));
159 assert_eq!(current.version, OUTPUT_VERSION);
160 assert_eq!(
161 serde_json::to_value(¤t).unwrap(),
162 value,
163 "{}",
164 path.display()
165 );
166 seen += 1;
167 }
168 assert!(seen >= 10);
169 }
170
171 #[test]
172 fn the_validator_mirrors_the_contract() {
173 validate_arg("--pinned-slice-us=500").unwrap();
174 for bad in ["", "-s 5", "$HOME", "'x'", "a;b"] {
175 assert!(validate_arg(bad).is_err(), "{bad:?}");
176 }
177 assert_eq!(
178 Next {
179 sched: "/usr/bin/scx_lavd".into(),
180 args: Vec::new(),
181 overridden: false
182 }
183 .name(),
184 "scx_lavd"
185 );
186 }
187}