1#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
15pub enum CapabilityState {
16 Available,
18 Unsupported,
20 PermissionDenied,
25 #[default]
27 Unknown,
28}
29
30impl CapabilityState {
31 #[must_use]
33 pub const fn label(self) -> &'static str {
34 match self {
35 Self::Available => "available",
36 Self::Unsupported => "unsupported",
37 Self::PermissionDenied => "permission denied",
38 Self::Unknown => "not probed",
39 }
40 }
41
42 #[must_use]
44 pub const fn symbol(self) -> char {
45 match self {
46 Self::Available => '+',
47 Self::Unsupported => '-',
48 Self::PermissionDenied => '!',
49 Self::Unknown => '?',
50 }
51 }
52
53 #[must_use]
58 pub const fn privileges_might_help(self) -> bool {
59 matches!(self, Self::PermissionDenied)
60 }
61}
62
63#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66pub struct CapabilitySnapshot {
67 pub per_process_io: CapabilityState,
69 pub per_process_threads: CapabilityState,
71 pub per_process_open_files: CapabilityState,
73 pub per_process_sockets: CapabilityState,
75 pub per_process_working_directory: CapabilityState,
77 pub per_core_cpu: CapabilityState,
79 pub cpu_breakdown: CapabilityState,
81 pub load_average: CapabilityState,
83 pub swap_activity: CapabilityState,
85 pub disk_io: CapabilityState,
87 pub disk_busy: CapabilityState,
89 pub filesystem_capacity: CapabilityState,
91 pub network_counters: CapabilityState,
93 pub network_link_speed: CapabilityState,
95 pub network_errors: CapabilityState,
97 pub temperatures: CapabilityState,
99 pub battery: CapabilityState,
101 pub linux_psi: CapabilityState,
103 pub cgroup_limits: CapabilityState,
105 pub kernel_threads: CapabilityState,
107 pub process_signals: CapabilityState,
109 pub renice: CapabilityState,
111}
112
113impl CapabilitySnapshot {
114 pub const COUNT: usize = 22;
116
117 #[must_use]
121 pub fn entries(&self) -> [(&'static str, CapabilityState); Self::COUNT] {
122 [
123 ("process I/O", self.per_process_io),
124 ("process threads", self.per_process_threads),
125 ("process open files", self.per_process_open_files),
126 ("process sockets", self.per_process_sockets),
127 (
128 "process working directory",
129 self.per_process_working_directory,
130 ),
131 ("per-core CPU", self.per_core_cpu),
132 ("CPU time breakdown", self.cpu_breakdown),
133 ("load average", self.load_average),
134 ("swap activity", self.swap_activity),
135 ("disk I/O", self.disk_io),
136 ("disk busy", self.disk_busy),
137 ("filesystem capacity", self.filesystem_capacity),
138 ("network counters", self.network_counters),
139 ("network link speed", self.network_link_speed),
140 ("network errors", self.network_errors),
141 ("temperatures", self.temperatures),
142 ("battery", self.battery),
143 ("Linux PSI", self.linux_psi),
144 ("cgroup limits", self.cgroup_limits),
145 ("kernel threads", self.kernel_threads),
146 ("process signals", self.process_signals),
147 ("renice", self.renice),
148 ]
149 }
150
151 #[must_use]
154 pub fn unavailable(&self) -> Vec<(&'static str, CapabilityState)> {
155 self.entries()
156 .into_iter()
157 .filter(|(_, state)| *state != CapabilityState::Available)
158 .collect()
159 }
160
161 #[must_use]
164 pub fn any_permission_denied(&self) -> bool {
165 self.entries()
166 .into_iter()
167 .any(|(_, state)| state.privileges_might_help())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn entries_covers_every_declared_capability() {
177 let capabilities = CapabilitySnapshot::default();
178 assert_eq!(capabilities.entries().len(), CapabilitySnapshot::COUNT);
179 }
180
181 #[test]
182 fn entry_labels_are_unique_and_ascii() {
183 let entries = CapabilitySnapshot::default().entries();
184 let mut labels: Vec<&str> = entries.iter().map(|(label, _)| *label).collect();
185 assert!(labels.iter().all(|label| label.is_ascii()));
186 labels.sort_unstable();
187 labels.dedup();
188 assert_eq!(
189 labels.len(),
190 CapabilitySnapshot::COUNT,
191 "duplicate capability label"
192 );
193 }
194
195 #[test]
196 fn an_unprobed_snapshot_reports_everything_as_unavailable() {
197 let capabilities = CapabilitySnapshot::default();
198 assert_eq!(capabilities.unavailable().len(), CapabilitySnapshot::COUNT);
199 assert!(
200 !capabilities.any_permission_denied(),
201 "unknown is not denied"
202 );
203 }
204
205 #[test]
206 fn permission_denied_is_distinct_from_unsupported() {
207 let capabilities = CapabilitySnapshot {
208 per_process_io: CapabilityState::PermissionDenied,
209 linux_psi: CapabilityState::Unsupported,
210 ..CapabilitySnapshot::default()
211 };
212
213 assert!(capabilities.any_permission_denied());
214 assert!(CapabilityState::PermissionDenied.privileges_might_help());
215 assert!(
216 !CapabilityState::Unsupported.privileges_might_help(),
217 "root cannot conjure a kernel feature that does not exist"
218 );
219 }
220
221 #[test]
222 fn available_capabilities_drop_out_of_the_unavailable_list() {
223 let capabilities = CapabilitySnapshot {
224 per_process_io: CapabilityState::Available,
225 ..CapabilitySnapshot::default()
226 };
227 let unavailable = capabilities.unavailable();
228 assert_eq!(unavailable.len(), CapabilitySnapshot::COUNT - 1);
229 assert!(!unavailable.iter().any(|(label, _)| *label == "process I/O"));
230 }
231
232 #[test]
233 fn state_symbols_are_distinguishable_without_color() {
234 let mut symbols: Vec<char> = [
235 CapabilityState::Available,
236 CapabilityState::Unsupported,
237 CapabilityState::PermissionDenied,
238 CapabilityState::Unknown,
239 ]
240 .iter()
241 .map(|state| state.symbol())
242 .collect();
243 symbols.sort_unstable();
244 symbols.dedup();
245 assert_eq!(symbols.len(), 4);
246 }
247}