1use std::fmt::{self, Display};
5use std::fs::read_to_string;
6use std::process::Command;
7
8use eyre::{eyre, Result};
9
10use crate::config::utils::{
11 device_id_is_valid, hardware_version_is_valid, software_type_is_valid,
12 software_version_is_valid,
13};
14use crate::util::etc_os_release::EtcOsRelease;
15
16const DEVICE_ID_PATH: &str = "/etc/machine-id";
17const HARDWARE_VERSION_COMMAND: &str = "uname";
18const HARDWARE_VERSION_ARGS: &[&str] = &["-n"];
19
20#[cfg_attr(test, mockall::automock)]
21pub trait DeviceInfoDefaults {
26 fn software_version(&self) -> Result<Option<String>>;
28
29 fn device_id(&self) -> Result<String>;
31
32 fn hardware_version(&self) -> Result<String>;
34
35 fn software_type(&self) -> Result<Option<String>>;
37}
38
39pub struct DeviceInfoDefaultsImpl {
41 os_release: Option<EtcOsRelease>,
42}
43
44impl DeviceInfoDefaultsImpl {
45 fn new(os_release: Option<EtcOsRelease>) -> Self {
46 Self { os_release }
47 }
48}
49
50impl DeviceInfoDefaults for DeviceInfoDefaultsImpl {
51 fn software_version(&self) -> Result<Option<String>> {
52 Ok(self.os_release.as_ref().and_then(|os| os.version_id()))
53 }
54
55 fn device_id(&self) -> Result<String> {
56 let device_id = read_to_string(DEVICE_ID_PATH)?;
57 if device_id.is_empty() {
58 return Err(eyre!("Empty device id ({})", DEVICE_ID_PATH));
59 }
60
61 Ok(device_id)
62 }
63
64 fn hardware_version(&self) -> Result<String> {
65 let output = Command::new(HARDWARE_VERSION_COMMAND)
66 .args(HARDWARE_VERSION_ARGS)
67 .output()?;
68
69 Ok(String::from_utf8(output.stdout)?)
70 }
71
72 fn software_type(&self) -> Result<Option<String>> {
73 Ok(self.os_release.as_ref().and_then(|os| os.id()))
74 }
75}
76
77#[derive(Debug, PartialEq, Eq)]
78pub enum DeviceInfoValue {
79 Configured(String),
80 Default(String),
81}
82
83impl AsRef<str> for DeviceInfoValue {
84 fn as_ref(&self) -> &str {
85 match self {
86 DeviceInfoValue::Configured(s) => s.as_ref(),
87 DeviceInfoValue::Default(s) => s.as_ref(),
88 }
89 }
90}
91
92impl Display for DeviceInfoValue {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{}", self.as_ref())
95 }
96}
97
98#[derive(Debug)]
99pub struct DeviceInfo {
100 pub device_id: String,
101 pub hardware_version: String,
102 pub software_version: Option<DeviceInfoValue>,
103 pub software_type: Option<DeviceInfoValue>,
104}
105
106#[derive(PartialEq, Eq, Debug)]
107pub struct DeviceInfoWarning {
108 line: Option<String>,
109 message: String,
110}
111
112impl std::fmt::Display for DeviceInfoWarning {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
114 match &self.line {
115 Some(line) => write!(f, "Skipped line: '{}' ({})", line, self.message),
116 None => write!(f, "{}", self.message),
117 }
118 }
119}
120
121impl DeviceInfo {
122 pub fn parse<T: DeviceInfoDefaults>(
123 output: Option<&[u8]>,
124 defaults: T,
125 ) -> Result<(DeviceInfo, Vec<DeviceInfoWarning>)> {
126 let mut warnings = vec![];
127
128 let mut device_id: Option<String> = None;
129 let mut hardware_version: Option<String> = None;
130 let mut software_version: Option<DeviceInfoValue> = None;
131 let mut software_type: Option<DeviceInfoValue> = None;
132
133 match output {
134 Some(output) => {
135 for line in std::str::from_utf8(output)?.lines() {
136 if let Some((key, value)) = line.split_once('=') {
137 match key {
138 "MEMFAULT_DEVICE_ID" => device_id = Some(value.into()),
139 "MEMFAULT_HARDWARE_VERSION" => hardware_version = Some(value.into()),
140 "MEMFAULT_SOFTWARE_VERSION" => {
141 software_version = Some(DeviceInfoValue::Configured(value.into()))
142 }
143 "MEMFAULT_SOFTWARE_TYPE" => {
144 software_type = Some(DeviceInfoValue::Configured(value.into()))
145 }
146 _ => warnings.push(DeviceInfoWarning {
147 line: Some(line.into()),
148 message: "Unknown variable.".to_string(),
149 }),
150 }
151 } else {
152 warnings.push(DeviceInfoWarning {
153 line: Some(line.into()),
154 message: "Expect '=' separated key/value pairs.".to_string(),
155 })
156 }
157 }
158 }
159 None => {
160 warnings.push(DeviceInfoWarning {
161 line: None,
162 message: "No output from memfault-device-info.".to_string(),
163 });
164 }
165 }
166
167 software_version = software_version.or_else(|| {
169 defaults
170 .software_version()
171 .unwrap_or_else(|_| {
172 warnings.push(DeviceInfoWarning {
173 line: None,
174 message: "Failed to get default software version.".to_string(),
175 });
176 None
177 })
178 .map(DeviceInfoValue::Default)
179 });
180 device_id = device_id.or_else(|| {
181 defaults.device_id().map_or_else(
182 |_| {
183 warnings.push(DeviceInfoWarning {
184 line: None,
185 message: format!("Failed to open {}", DEVICE_ID_PATH),
186 });
187 None
188 },
189 |id| Some(id.trim().to_string()),
190 )
191 });
192 hardware_version = hardware_version.or_else(|| {
193 defaults.hardware_version().map_or_else(
194 |_| {
195 warnings.push(DeviceInfoWarning {
196 line: None,
197 message: format!(
198 "Failed to to get hardware version from: '{}'",
199 HARDWARE_VERSION_COMMAND
200 ),
201 });
202 None
203 },
204 |hwv| Some(hwv.trim().to_string()),
205 )
206 });
207 software_type = software_type.or_else(|| {
208 defaults
209 .software_type()
210 .unwrap_or_else(|_| {
211 warnings.push(DeviceInfoWarning {
212 line: None,
213 message: "Failed to get default software_type.".to_string(),
214 });
215 None
216 })
217 .map(DeviceInfoValue::Default)
218 });
219
220 let di = DeviceInfo {
221 device_id: device_id.ok_or(eyre!("No device id supplied"))?,
222 hardware_version: hardware_version.ok_or(eyre!("No hardware version supplied"))?,
223 software_version,
224 software_type,
225 };
226
227 let validation_errors: Vec<String> = [
229 (
230 "MEMFAULT_HARDWARE_VERSION",
231 hardware_version_is_valid(&di.hardware_version),
232 ),
233 (
234 "MEMFAULT_SOFTWARE_VERSION",
235 di.software_version
236 .as_ref()
237 .map_or(Ok(()), |swv| software_version_is_valid(swv.as_ref())),
238 ),
239 (
240 "MEMFAULT_SOFTWARE_TYPE",
241 di.software_type
242 .as_ref()
243 .map_or(Ok(()), |swt| software_type_is_valid(swt.as_ref())),
244 ),
245 ("MEMFAULT_DEVICE_ID", device_id_is_valid(&di.device_id)),
246 ]
247 .iter()
248 .filter_map(|(key, result)| match result {
249 Err(e) => Some(format!(" Invalid {}: {}", key, e)),
250 _ => None,
251 })
252 .collect();
253
254 match validation_errors.is_empty() {
255 true => Ok((di, warnings)),
256 false => Err(eyre::eyre!("\n{}", validation_errors.join("\n"))),
257 }
258 }
259
260 pub fn load() -> eyre::Result<(DeviceInfo, Vec<DeviceInfoWarning>)> {
261 let user_output = Command::new("memfault-device-info").output().ok();
262 let stdout = user_output.as_ref().map(|o| o.stdout.as_slice());
263
264 let os_release = EtcOsRelease::load().ok();
265 let di_defaults = DeviceInfoDefaultsImpl::new(os_release);
266 Self::parse(stdout, di_defaults)
267 }
268}
269
270#[cfg(test)]
271impl DeviceInfo {
272 pub fn test_fixture() -> Self {
273 DeviceInfo {
274 device_id: "001".to_owned(),
275 hardware_version: "DVT".to_owned(),
276 software_version: None,
277 software_type: None,
278 }
279 }
280
281 pub fn test_fixture_with_overrides(software_version: &str, software_type: &str) -> Self {
282 DeviceInfo {
283 device_id: "001".to_owned(),
284 hardware_version: "DVT".to_owned(),
285 software_version: Some(DeviceInfoValue::Configured(software_version.into())),
286 software_type: Some(DeviceInfoValue::Configured(software_type.into())),
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use rstest::rstest;
295
296 #[test]
297 fn test_empty() {
298 let mut di_defaults = MockDeviceInfoDefaults::new();
299 di_defaults
300 .expect_software_type()
301 .returning(|| Err(eyre!("")));
302 di_defaults.expect_software_version().returning(|| Ok(None));
303 di_defaults
304 .expect_hardware_version()
305 .returning(|| Err(eyre!("")));
306 di_defaults
307 .expect_device_id()
308 .returning(|| Ok("123ABC".into()));
309 let r = DeviceInfo::parse(Some(b""), di_defaults);
310 assert!(r.is_err())
311 }
312
313 #[test]
314 fn test_with_warnings() {
315 let mut di_defaults = MockDeviceInfoDefaults::new();
316 di_defaults.expect_software_type().returning(|| Ok(None));
317 di_defaults.expect_software_version().returning(|| Ok(None));
318 di_defaults
319 .expect_device_id()
320 .returning(|| Ok("123ABC".into()));
321 di_defaults
322 .expect_hardware_version()
323 .returning(|| Ok("Hardware".into()));
324 let r = DeviceInfo::parse(
325 Some(b"MEMFAULT_DEVICE_ID=X\nMEMFAULT_HARDWARE_VERSION=Y\nblahblahblah\n"),
326 di_defaults,
327 );
328 assert!(r.is_ok());
329
330 let (di, warnings) = r.unwrap();
331 assert_eq!(di.device_id, "X");
332 assert_eq!(di.hardware_version, "Y");
333 assert_eq!(warnings.len(), 1);
334 assert_eq!(
335 warnings[0],
336 DeviceInfoWarning {
337 line: Some("blahblahblah".into()),
338 message: "Expect '=' separated key/value pairs.".to_string()
339 }
340 );
341 }
342
343 #[rstest]
344 #[case(b"MEMFAULT_DEVICE_ID=123ABC\nMEMFAULT_HARDWARE_VERSION=1.0.0\nMEMFAULT_SOFTWARE_VERSION=1.2.3\n", Some("1.2.3".into()), None)]
346 #[case(b"MEMFAULT_DEVICE_ID=123ABC\nMEMFAULT_HARDWARE_VERSION=1.0.0\nMEMFAULT_SOFTWARE_TYPE=test\n", None, Some("test".into()))]
348 #[case(b"MEMFAULT_DEVICE_ID=123ABC\nMEMFAULT_HARDWARE_VERSION=1.0.0\nMEMFAULT_SOFTWARE_VERSION=1.2.3\nMEMFAULT_SOFTWARE_TYPE=test\n", Some("1.2.3".into()), Some("test".into()))]
350 fn test_with_sw_version_and_type(
351 #[case] output: &[u8],
352 #[case] sw_version: Option<String>,
353 #[case] sw_type: Option<String>,
354 ) {
355 let mut di_defaults = MockDeviceInfoDefaults::new();
356 di_defaults.expect_software_type().returning(|| Ok(None));
357 di_defaults.expect_software_version().returning(|| Ok(None));
358 di_defaults
359 .expect_hardware_version()
360 .returning(|| Ok("Hardware".into()));
361 di_defaults
362 .expect_device_id()
363 .returning(|| Ok("123ABC".into()));
364 let r = DeviceInfo::parse(Some(output), di_defaults);
365 assert!(r.is_ok());
366
367 let (di, warnings) = r.unwrap();
368 assert_eq!(di.device_id, "123ABC");
369 assert_eq!(di.hardware_version, "1.0.0");
370 assert_eq!(
371 di.software_version,
372 sw_version.map(DeviceInfoValue::Configured)
373 );
374 assert_eq!(di.software_type, sw_type.map(DeviceInfoValue::Configured));
375
376 assert_eq!(warnings.len(), 0);
377 }
378
379 #[rstest]
380 #[case::default_with_no_response(
381 Some("1.2.3".to_string()),
382 Some(DeviceInfoValue::Default("1.2.3".to_string())),
383 b""
384 )]
385 #[case::default_with_response(
386 Some("1.2.3".to_string()),
387 Some(DeviceInfoValue::Configured("1.2.4".to_string())),
388 b"MEMFAULT_SOFTWARE_VERSION=1.2.4"
389 )]
390 #[case::no_default_with_response(
391 None,
392 Some(DeviceInfoValue::Configured("1.2.4".to_string())),
393 b"MEMFAULT_SOFTWARE_VERSION=1.2.4"
394 )]
395 #[case::no_default_no_response(None, None, b"")]
396 fn test_with_default_swv(
397 #[case] software_version_default: Option<String>,
398 #[case] expected: Option<DeviceInfoValue>,
399 #[case] output: &[u8],
400 ) {
401 let mut output_required =
403 b"MEMFAULT_DEVICE_ID=DEVICE\nMEMFAULT_HARDWARE_VERSION=HARDWARE\n".to_vec();
404 output_required.extend(output);
405
406 let mut di_defaults = MockDeviceInfoDefaults::new();
407 di_defaults
408 .expect_software_type()
409 .returning(|| Err(eyre!("")));
410 di_defaults
411 .expect_software_version()
412 .returning(move || Ok(software_version_default.clone()));
413 di_defaults
414 .expect_hardware_version()
415 .returning(|| Err(eyre!("")));
416 di_defaults.expect_device_id().returning(|| Err(eyre!("")));
417
418 let (di, _warnings) = DeviceInfo::parse(Some(&output_required), di_defaults).unwrap();
419 assert_eq!(di.software_version, expected);
420 }
421
422 #[rstest]
423 #[case::default_with_no_response(Some("123ABC".to_string()), Some(DeviceInfoValue::Default("123ABC".to_string())), b"")]
424 #[case::default_with_response(Some("123ABC".to_string()), Some(DeviceInfoValue::Configured("main".to_string())), b"MEMFAULT_SOFTWARE_TYPE=main")]
425 #[case::no_default_with_response(None, Some(DeviceInfoValue::Configured("main".to_string())), b"MEMFAULT_SOFTWARE_TYPE=main")]
426 #[case::no_default_no_response(None, None, b"")]
427 fn test_with_default_sw_type(
428 #[case] software_type_default: Option<String>,
429 #[case] expected: Option<DeviceInfoValue>,
430 #[case] output: &[u8],
431 ) {
432 let mut output_required =
434 b"MEMFAULT_DEVICE_ID=DEVICE\nMEMFAULT_HARDWARE_VERSION=HARDWARE\n".to_vec();
435 output_required.extend(output);
436
437 let mut di_defaults = MockDeviceInfoDefaults::new();
438 di_defaults
439 .expect_software_version()
440 .returning(|| Err(eyre!("")));
441 di_defaults
442 .expect_hardware_version()
443 .returning(|| Err(eyre!("")));
444 di_defaults.expect_device_id().returning(|| Err(eyre!("")));
445 di_defaults
446 .expect_software_type()
447 .returning(move || Ok(software_type_default.clone()));
448
449 let (di, _warnings) = DeviceInfo::parse(Some(&output_required), di_defaults).unwrap();
450 assert_eq!(di.software_type, expected);
451 }
452
453 #[rstest]
454 #[case::default_with_no_response(Some("123ABC".to_string()), Some("123ABC".to_string()), b"")]
455 #[case::default_with_whitespace(Some("123ABC\n".to_string()), Some("123ABC".to_string()), b"")]
456 #[case::default_with_response(Some("123ABC".to_string()), Some("DEVICE".to_string()), b"MEMFAULT_DEVICE_ID=DEVICE")]
457 #[case::no_default_with_response(None, Some("DEVICE".to_string()), b"MEMFAULT_DEVICE_ID=DEVICE")]
458 #[case::no_default_no_response(None, None, b"")]
459 fn test_with_default_device_id(
460 #[case] device_id_default: Option<String>,
461 #[case] expected: Option<String>,
462 #[case] output: &[u8],
463 ) {
464 let mut output_required = b"MEMFAULT_HARDWARE_VERSION=HARDWARE\n".to_vec();
466 output_required.extend(output);
467
468 let mut di_defaults = MockDeviceInfoDefaults::new();
469 di_defaults
470 .expect_software_type()
471 .returning(|| Err(eyre!("")));
472 di_defaults
473 .expect_software_version()
474 .returning(|| Err(eyre!("")));
475 di_defaults
476 .expect_hardware_version()
477 .returning(|| Err(eyre!("")));
478 di_defaults
479 .expect_device_id()
480 .returning(move || device_id_default.clone().ok_or(eyre!("")));
481
482 let ret = DeviceInfo::parse(Some(&output_required), di_defaults);
483 if let Some(expected) = expected {
484 let (di, _warnings) = ret.unwrap();
485 assert_eq!(di.device_id, expected);
486 } else {
487 assert!(ret.is_err());
488 }
489 }
490
491 #[rstest]
492 #[case::default_with_no_response(Some("123ABC".to_string()), Some("123ABC".to_string()), b"")]
493 #[case::default_with_whitespace(Some("123ABC\n".to_string()), Some("123ABC".to_string()), b"")]
494 #[case::default_with_response(Some("123ABC".to_string()), Some("HARDWARE".to_string()), b"MEMFAULT_HARDWARE_VERSION=HARDWARE")]
495 #[case::no_default_with_response(None, Some("HARDWARE".to_string()), b"MEMFAULT_HARDWARE_VERSION=HARDWARE")]
496 #[case::no_default_no_response(None, None, b"")]
497 fn test_with_default_hardware_version(
498 #[case] hardware_version_default: Option<String>,
499 #[case] expected: Option<String>,
500 #[case] output: &[u8],
501 ) {
502 let mut output_required = b"MEMFAULT_DEVICE_ID=DEVICE\n".to_vec();
504 output_required.extend(output);
505
506 let mut di_defaults = MockDeviceInfoDefaults::new();
507 di_defaults
508 .expect_software_type()
509 .returning(|| Err(eyre!("")));
510 di_defaults
511 .expect_software_version()
512 .returning(|| Err(eyre!("")));
513 di_defaults.expect_device_id().returning(|| Err(eyre!("")));
514 di_defaults
515 .expect_hardware_version()
516 .returning(move || hardware_version_default.clone().ok_or(eyre!("")));
517
518 let ret = DeviceInfo::parse(Some(&output_required), di_defaults);
519 if let Some(expected) = expected {
520 let (di, _warnings) = ret.unwrap();
521 assert_eq!(di.hardware_version, expected);
522 } else {
523 assert!(ret.is_err());
524 }
525 }
526
527 #[rstest]
528 fn test_with_no_device_info() {
529 let expected_software_type = "SOFTWARE_TYPE".to_string();
530 let expected_software_version = "SOFTWARE_VERSION".to_string();
531 let expected_hardware_version = "HARDWARE_VERSION".to_string();
532 let expected_device_id = "DEVICE_ID".to_string();
533
534 let mut di_defaults = MockDeviceInfoDefaults::new();
535 let default_software_type = expected_software_type.clone();
536 di_defaults
537 .expect_software_type()
538 .returning(move || Ok(Some(default_software_type.clone())));
539 let default_software_version = expected_software_version.clone();
540 di_defaults
541 .expect_software_version()
542 .returning(move || Ok(Some(default_software_version.clone())));
543 let default_hardware_version = expected_hardware_version.clone();
544 di_defaults
545 .expect_hardware_version()
546 .returning(move || Ok(default_hardware_version.clone()));
547 let default_device_id = expected_device_id.clone();
548 di_defaults
549 .expect_device_id()
550 .returning(move || Ok(default_device_id.clone()));
551
552 let r = DeviceInfo::parse(None, di_defaults).unwrap();
553
554 assert_eq!(
555 r.0.software_type,
556 Some(DeviceInfoValue::Default(expected_software_type))
557 );
558 assert_eq!(
559 r.0.software_version,
560 Some(DeviceInfoValue::Default(expected_software_version))
561 );
562 assert_eq!(r.0.hardware_version, expected_hardware_version);
563 assert_eq!(r.0.device_id, expected_device_id);
564 }
565
566 #[rstest]
567 fn test_no_default_calls_with_device_info() {
568 let expected_software_type = "SOFTWARE_TYPE".to_string();
569 let expected_software_version = "SOFTWARE_VERSION".to_string();
570 let expected_hardware_version = "HARDWARE_VERSION".to_string();
571 let expected_device_id = "DEVICE_ID".to_string();
572
573 let mut di_defaults = MockDeviceInfoDefaults::new();
574 di_defaults.expect_software_type().never();
575 di_defaults.expect_software_version().never();
576 di_defaults.expect_hardware_version().never();
577 di_defaults.expect_device_id().never();
578
579 let output = format!(
580 "MEMFAULT_DEVICE_ID={}\nMEMFAULT_HARDWARE_VERSION={}\nMEMFAULT_SOFTWARE_VERSION={}\nMEMFAULT_SOFTWARE_TYPE={}\n",
581 expected_device_id,
582 expected_hardware_version,
583 expected_software_version,
584 expected_software_type,
585 );
586
587 let r = DeviceInfo::parse(Some(output.as_bytes()), di_defaults).unwrap();
588
589 assert_eq!(
590 r.0.software_type,
591 Some(DeviceInfoValue::Configured(expected_software_type))
592 );
593 assert_eq!(
594 r.0.software_version,
595 Some(DeviceInfoValue::Configured(expected_software_version))
596 );
597 assert_eq!(r.0.hardware_version, expected_hardware_version);
598 assert_eq!(r.0.device_id, expected_device_id);
599 }
600}