1use serde::de::Error as DeError;
2use serde::{Deserialize, Deserializer, Serialize};
3use serde_json::Value;
4use std::process::Command;
5use std::slice::Iter;
6use std::string::FromUtf8Error;
7use std::vec::IntoIter;
8use thiserror::Error;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct MajMin {
13 pub major: u32,
15 pub minor: u32,
17}
18
19impl Serialize for MajMin {
20 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
21 where
22 S: serde::Serializer,
23 {
24 serializer.serialize_str(&format!("{}:{}", self.major, self.minor))
25 }
26}
27
28impl<'de> Deserialize<'de> for MajMin {
29 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
30 where
31 D: Deserializer<'de>,
32 {
33 let s = String::deserialize(deserializer)?;
34 let parts: Vec<&str> = s.split(':').collect();
35 if parts.len() != 2 {
36 return Err(DeError::custom(format!(
37 "invalid maj:min format: expected 'major:minor', got '{s}'"
38 )));
39 }
40 let major = parts[0]
41 .parse()
42 .map_err(|_| DeError::custom(format!("invalid major number: {}", parts[0])))?;
43 let minor = parts[1]
44 .parse()
45 .map_err(|_| DeError::custom(format!("invalid minor number: {}", parts[1])))?;
46 Ok(MajMin { major, minor })
47 }
48}
49
50impl std::fmt::Display for MajMin {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{}:{}", self.major, self.minor)
53 }
54}
55
56#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
58#[serde(rename_all = "lowercase")]
59pub enum DeviceType {
60 Disk,
62 Part,
64 Loop,
66 Raid1,
68 Raid5,
70 Raid6,
72 Raid0,
74 Raid10,
76 Lvm,
78 Crypt,
80 Rom,
82 #[serde(other)]
84 Other,
85}
86
87#[derive(Debug, Error)]
89pub enum BlockDevError {
90 #[error("failed to execute lsblk: {0}")]
92 CommandFailed(#[from] std::io::Error),
93
94 #[error("lsblk returned error: {0}")]
96 LsblkError(String),
97
98 #[error("invalid UTF-8 in lsblk output: {0}")]
100 InvalidUtf8(#[from] FromUtf8Error),
101
102 #[error("failed to parse lsblk JSON: {0}")]
104 JsonParse(#[from] serde_json::Error),
105}
106
107#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
109pub struct BlockDevices {
110 pub blockdevices: Vec<BlockDevice>,
112}
113
114fn parse_size_string(s: &str) -> Option<u64> {
116 let s = s.trim();
117 if s.is_empty() {
118 return None;
119 }
120
121 let (num_part, suffix) = {
123 let idx = s
124 .find(|c: char| !c.is_ascii_digit() && c != '.')
125 .unwrap_or(s.len());
126 (&s[..idx], s[idx..].trim())
127 };
128
129 let num: f64 = num_part.parse().ok()?;
130 let multiplier: u64 = match suffix.to_uppercase().as_str() {
131 "" | "B" => 1,
132 "K" | "KB" | "KIB" => 1024,
133 "M" | "MB" | "MIB" => 1024 * 1024,
134 "G" | "GB" | "GIB" => 1024 * 1024 * 1024,
135 "T" | "TB" | "TIB" => 1024 * 1024 * 1024 * 1024,
136 "P" | "PB" | "PIB" => 1024 * 1024 * 1024 * 1024 * 1024,
137 _ => return None,
138 };
139
140 Some((num * multiplier as f64) as u64)
141}
142
143fn deserialize_size<'de, D>(deserializer: D) -> Result<u64, D::Error>
145where
146 D: Deserializer<'de>,
147{
148 let value = Value::deserialize(deserializer)?;
149 match &value {
150 Value::Number(n) => n
151 .as_u64()
152 .or_else(|| n.as_f64().map(|f| f as u64))
153 .ok_or_else(|| DeError::custom("invalid numeric size")),
154 Value::String(s) => {
155 parse_size_string(s).ok_or_else(|| DeError::custom(format!("invalid size string: {s}")))
156 }
157 _ => Err(DeError::custom("size must be a number or string")),
158 }
159}
160
161fn deserialize_mountpoints<'de, D>(deserializer: D) -> Result<Vec<Option<String>>, D::Error>
179where
180 D: Deserializer<'de>,
181{
182 let value = Value::deserialize(deserializer)?;
183 if value.is_array() {
184 serde_json::from_value(value).map_err(DeError::custom)
186 } else {
187 let single: Option<String> = serde_json::from_value(value).map_err(DeError::custom)?;
189 Ok(vec![single])
190 }
191}
192
193#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
208pub struct BlockDevice {
209 pub name: String,
211 #[serde(rename = "maj:min")]
215 pub maj_min: MajMin,
216 pub rm: bool,
218 #[serde(deserialize_with = "deserialize_size")]
220 pub size: u64,
221 pub ro: bool,
223 #[serde(rename = "type")]
227 pub device_type: DeviceType,
228 #[serde(
232 default,
233 alias = "mountpoint",
234 deserialize_with = "deserialize_mountpoints"
235 )]
236 pub mountpoints: Vec<Option<String>>,
237 #[serde(default)]
239 pub children: Option<Vec<BlockDevice>>,
240}
241
242impl BlockDevice {
243 #[must_use]
245 pub fn has_children(&self) -> bool {
246 self.children.as_ref().is_some_and(|c| !c.is_empty())
247 }
248
249 pub fn children_iter(&self) -> impl Iterator<Item = &BlockDevice> {
253 self.children.iter().flat_map(|c| c.iter())
254 }
255
256 #[must_use]
260 pub fn find_child(&self, name: &str) -> Option<&BlockDevice> {
261 self.children.as_ref()?.iter().find(|c| c.name == name)
262 }
263
264 #[must_use]
266 pub fn active_mountpoints(&self) -> Vec<&str> {
267 self.mountpoints
268 .iter()
269 .filter_map(|m| m.as_deref())
270 .collect()
271 }
272
273 #[must_use]
275 pub fn is_mounted(&self) -> bool {
276 self.mountpoints.iter().any(|m| m.is_some())
277 }
278
279 #[must_use]
282 pub fn is_system(&self) -> bool {
283 if self.mountpoints.iter().any(|m| m.as_deref() == Some("/")) {
284 return true;
285 }
286 if let Some(children) = &self.children {
287 for child in children {
288 if child.is_system() {
289 return true;
290 }
291 }
292 }
293 false
294 }
295
296 #[must_use]
298 pub fn is_disk(&self) -> bool {
299 self.device_type == DeviceType::Disk
300 }
301
302 #[must_use]
304 pub fn is_partition(&self) -> bool {
305 self.device_type == DeviceType::Part
306 }
307}
308
309impl BlockDevices {
310 #[must_use]
312 pub fn len(&self) -> usize {
313 self.blockdevices.len()
314 }
315
316 #[must_use]
318 pub fn is_empty(&self) -> bool {
319 self.blockdevices.is_empty()
320 }
321
322 pub fn iter(&self) -> Iter<'_, BlockDevice> {
324 self.blockdevices.iter()
325 }
326
327 #[must_use]
330 pub fn system(&self) -> Vec<&BlockDevice> {
331 self.blockdevices
332 .iter()
333 .filter(|device| device.is_system())
334 .collect()
335 }
336
337 #[must_use]
340 pub fn non_system(&self) -> Vec<&BlockDevice> {
341 self.blockdevices
342 .iter()
343 .filter(|device| !device.is_system())
344 .collect()
345 }
346
347 #[must_use]
351 pub fn find_by_name(&self, name: &str) -> Option<&BlockDevice> {
352 self.blockdevices.iter().find(|d| d.name == name)
353 }
354}
355
356impl IntoIterator for BlockDevices {
357 type Item = BlockDevice;
358 type IntoIter = IntoIter<BlockDevice>;
359
360 fn into_iter(self) -> Self::IntoIter {
361 self.blockdevices.into_iter()
362 }
363}
364
365impl<'a> IntoIterator for &'a BlockDevices {
366 type Item = &'a BlockDevice;
367 type IntoIter = Iter<'a, BlockDevice>;
368
369 fn into_iter(self) -> Self::IntoIter {
370 self.blockdevices.iter()
371 }
372}
373
374pub fn parse_lsblk(json_data: &str) -> Result<BlockDevices, serde_json::Error> {
398 serde_json::from_str(json_data)
399}
400
401pub fn get_devices() -> Result<BlockDevices, BlockDevError> {
416 let output = Command::new("lsblk")
417 .arg("--json")
418 .arg("--bytes")
419 .output()?;
420
421 if !output.status.success() {
422 return Err(BlockDevError::LsblkError(
423 String::from_utf8_lossy(&output.stderr).into_owned(),
424 ));
425 }
426
427 let json_output = String::from_utf8(output.stdout)?;
428 let lsblk = parse_lsblk(&json_output)?;
429 Ok(lsblk)
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 const SAMPLE_JSON: &str = r#"
437 {
438 "blockdevices": [
439 {"name":"nvme1n1", "maj:min":"259:0", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
440 "children": [
441 {"name":"nvme1n1p1", "maj:min":"259:1", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
442 {"name":"nvme1n1p9", "maj:min":"259:2", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
443 ]
444 },
445 {"name":"nvme7n1", "maj:min":"259:3", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
446 "children": [
447 {"name":"nvme7n1p1", "maj:min":"259:7", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
448 {"name":"nvme7n1p9", "maj:min":"259:8", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
449 ]
450 },
451 {"name":"nvme5n1", "maj:min":"259:4", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
452 "children": [
453 {"name":"nvme5n1p1", "maj:min":"259:5", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
454 {"name":"nvme5n1p9", "maj:min":"259:6", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
455 ]
456 },
457 {"name":"nvme9n1", "maj:min":"259:9", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
458 "children": [
459 {"name":"nvme9n1p1", "maj:min":"259:13", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
460 {"name":"nvme9n1p9", "maj:min":"259:14", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
461 ]
462 },
463 {"name":"nvme4n1", "maj:min":"259:10", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
464 "children": [
465 {"name":"nvme4n1p1", "maj:min":"259:11", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
466 {"name":"nvme4n1p9", "maj:min":"259:12", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
467 ]
468 },
469 {"name":"nvme8n1", "maj:min":"259:15", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
470 "children": [
471 {"name":"nvme8n1p1", "maj:min":"259:20", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
472 {"name":"nvme8n1p9", "maj:min":"259:21", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
473 ]
474 },
475 {"name":"nvme6n1", "maj:min":"259:16", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
476 "children": [
477 {"name":"nvme6n1p1", "maj:min":"259:17", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
478 {"name":"nvme6n1p9", "maj:min":"259:18", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
479 ]
480 },
481 {"name":"nvme3n1", "maj:min":"259:19", "rm":false, "size":"894.3G", "ro":false, "type":"disk", "mountpoint":null,
482 "children": [
483 {"name":"nvme3n1p1", "maj:min":"259:23", "rm":false, "size":"1M", "ro":false, "type":"part", "mountpoint":null},
484 {"name":"nvme3n1p2", "maj:min":"259:24", "rm":false, "size":"244M", "ro":false, "type":"part", "mountpoint":"/boot/efi"},
485 {"name":"nvme3n1p3", "maj:min":"259:25", "rm":false, "size":"488M", "ro":false, "type":"part", "mountpoint":null,
486 "children": [
487 {"name":"md0", "maj:min":"9:0", "rm":false, "size":"487M", "ro":false, "type":"raid1", "mountpoint":"/boot"}
488 ]
489 },
490 {"name":"nvme3n1p4", "maj:min":"259:26", "rm":false, "size":"7.6G", "ro":false, "type":"part", "mountpoint":null,
491 "children": [
492 {"name":"md1", "maj:min":"9:1", "rm":false, "size":"7.6G", "ro":false, "type":"raid1", "mountpoint":"[SWAP]"}
493 ]
494 },
495 {"name":"nvme3n1p5", "maj:min":"259:27", "rm":false, "size":"19.1G", "ro":false, "type":"part", "mountpoint":null,
496 "children": [
497 {"name":"md2", "maj:min":"9:2", "rm":false, "size":"19.1G", "ro":false, "type":"raid1", "mountpoint":"/"}
498 ]
499 },
500 {"name":"nvme3n1p6", "maj:min":"259:28", "rm":false, "size":"866.8G", "ro":false, "type":"part", "mountpoint":null}
501 ]
502 },
503 {"name":"nvme0n1", "maj:min":"259:22", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
504 "children": [
505 {"name":"nvme0n1p1", "maj:min":"259:29", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
506 {"name":"nvme0n1p9", "maj:min":"259:30", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
507 ]
508 },
509 {"name":"nvme2n1", "maj:min":"259:31", "rm":false, "size":"894.3G", "ro":false, "type":"disk", "mountpoint":null,
510 "children": [
511 {"name":"nvme2n1p1", "maj:min":"259:32", "rm":false, "size":"1M", "ro":false, "type":"part", "mountpoint":null},
512 {"name":"nvme2n1p2", "maj:min":"259:33", "rm":false, "size":"244M", "ro":false, "type":"part", "mountpoint":null},
513 {"name":"nvme2n1p3", "maj:min":"259:34", "rm":false, "size":"488M", "ro":false, "type":"part", "mountpoint":null,
514 "children": [
515 {"name":"md0", "maj:min":"9:0", "rm":false, "size":"487M", "ro":false, "type":"raid1", "mountpoint":"/boot"}
516 ]
517 },
518 {"name":"nvme2n1p4", "maj:min":"259:35", "rm":false, "size":"7.6G", "ro":false, "type":"part", "mountpoint":null,
519 "children": [
520 {"name":"md1", "maj:min":"9:1", "rm":false, "size":"7.6G", "ro":false, "type":"raid1", "mountpoint":"[SWAP]"}
521 ]
522 },
523 {"name":"nvme2n1p5", "maj:min":"259:36", "rm":false, "size":"19.1G", "ro":false, "type":"part", "mountpoint":null,
524 "children": [
525 {"name":"md2", "maj:min":"9:2", "rm":false, "size":"19.1G", "ro":false, "type":"raid1", "mountpoint":"/"}
526 ]
527 },
528 {"name":"nvme2n1p6", "maj:min":"259:37", "rm":false, "size":"866.8G", "ro":false, "type":"part", "mountpoint":null}
529 ]
530 }
531 ]
532 }
533 "#;
534
535 #[test]
536 fn test_parse_lsblk() {
537 let lsblk = parse_lsblk(SAMPLE_JSON).expect("Failed to parse JSON");
538
539 assert_eq!(
541 lsblk.blockdevices.len(),
542 10,
543 "Expected 10 top-level block devices"
544 );
545
546 for device in &lsblk.blockdevices {
548 assert!(!device.name.is_empty(), "Device name should not be empty");
549 }
550
551 let nvme3n1 = lsblk
553 .blockdevices
554 .iter()
555 .find(|d| d.name == "nvme3n1")
556 .expect("Expected to find device nvme3n1");
557
558 assert!(
560 nvme3n1
561 .mountpoints
562 .first()
563 .and_then(|opt| opt.as_deref())
564 .is_none(),
565 "nvme3n1 effective mountpoint should be None"
566 );
567
568 let children = nvme3n1
570 .children
571 .as_ref()
572 .expect("nvme3n1 should have children");
573 assert_eq!(children.len(), 6, "nvme3n1 should have 6 children");
574
575 let nvme3n1p2 = children
577 .iter()
578 .find(|c| c.name == "nvme3n1p2")
579 .expect("Expected to find nvme3n1p2");
580 assert_eq!(
581 nvme3n1p2.mountpoints.first().and_then(|opt| opt.as_deref()),
582 Some("/boot/efi"),
583 "nvme3n1p2 first mountpoint should be '/boot/efi'"
584 );
585
586 let nvme3n1p3 = children
588 .iter()
589 .find(|c| c.name == "nvme3n1p3")
590 .expect("Expected to find nvme3n1p3");
591 let nested_children = nvme3n1p3
592 .children
593 .as_ref()
594 .expect("nvme3n1p3 should have children");
595 let md0 = nested_children
596 .iter()
597 .find(|d| d.name == "md0")
598 .expect("Expected to find md0 under nvme3n1p3");
599 assert_eq!(
600 md0.mountpoints.first().and_then(|opt| opt.as_deref()),
601 Some("/boot"),
602 "md0 effective mountpoint should be '/boot'"
603 );
604
605 let non_system = lsblk.non_system();
608 assert_eq!(
609 non_system.len(),
610 8,
611 "Expected 8 non-system top-level devices, since nvme3n1/nvme2n1 is system"
612 );
613 assert!(
614 !non_system.iter().any(|d| d.name == "nvme3n1"),
615 "nvme3n1 should be excluded from non-system devices"
616 );
617 }
618
619 #[test]
620 fn test_non_system() {
621 let test_json = r#"
624 {
625 "blockdevices": [
626 {
627 "name": "sda",
628 "maj:min": "8:0",
629 "rm": false,
630 "size": "447.1G",
631 "ro": false,
632 "type": "disk",
633 "mountpoints": [
634 null
635 ],
636 "children": [
637 {
638 "name": "sda1",
639 "maj:min": "8:1",
640 "rm": false,
641 "size": "512M",
642 "ro": false,
643 "type": "part",
644 "mountpoints": [
645 null
646 ]
647 },{
648 "name": "sda2",
649 "maj:min": "8:2",
650 "rm": false,
651 "size": "446.6G",
652 "ro": false,
653 "type": "part",
654 "mountpoints": [
655 null
656 ],
657 "children": [
658 {
659 "name": "md0",
660 "maj:min": "9:0",
661 "rm": false,
662 "size": "446.6G",
663 "ro": false,
664 "type": "raid1",
665 "mountpoints": [
666 "/"
667 ]
668 }
669 ]
670 }
671 ]
672 },{
673 "name": "sdb",
674 "maj:min": "8:16",
675 "rm": false,
676 "size": "447.1G",
677 "ro": false,
678 "type": "disk",
679 "mountpoints": [
680 null
681 ],
682 "children": [
683 {
684 "name": "sdb1",
685 "maj:min": "8:17",
686 "rm": false,
687 "size": "512M",
688 "ro": false,
689 "type": "part",
690 "mountpoints": [
691 "/boot/efi"
692 ]
693 },{
694 "name": "sdb2",
695 "maj:min": "8:18",
696 "rm": false,
697 "size": "446.6G",
698 "ro": false,
699 "type": "part",
700 "mountpoints": [
701 null
702 ],
703 "children": [
704 {
705 "name": "md0",
706 "maj:min": "9:0",
707 "rm": false,
708 "size": "446.6G",
709 "ro": false,
710 "type": "raid1",
711 "mountpoints": [
712 "/"
713 ]
714 }
715 ]
716 }
717 ]
718 },{
719 "name": "nvme0n1",
720 "maj:min": "259:2",
721 "rm": false,
722 "size": "1.7T",
723 "ro": false,
724 "type": "disk",
725 "mountpoints": [
726 null
727 ]
728 },{
729 "name": "nvme1n1",
730 "maj:min": "259:3",
731 "rm": false,
732 "size": "1.7T",
733 "ro": false,
734 "type": "disk",
735 "mountpoints": [
736 null
737 ]
738 }
739 ]
740 }
741 "#;
742 let disks = parse_lsblk(test_json).unwrap();
743 let non_system = disks.non_system();
744 assert_eq!(non_system.len(), 2);
745 let names: Vec<&str> = non_system.iter().map(|d| d.name.as_str()).collect();
746 assert_eq!(names, vec!["nvme0n1", "nvme1n1"]);
747 }
748
749 #[test]
752 #[ignore = "requires lsblk command to be available on the system"]
753 fn test_get_devices() {
754 let dev = get_devices().expect("Failed to get block devices");
755 assert!(!dev.blockdevices.is_empty());
757 }
758 #[test]
759 fn test_into_iterator() {
760 let device1 = BlockDevice {
762 name: "sda".to_string(),
763 maj_min: MajMin { major: 8, minor: 0 },
764 rm: false,
765 size: 536_870_912_000, ro: false,
767 device_type: DeviceType::Disk,
768 mountpoints: vec![None],
769 children: None,
770 };
771
772 let device2 = BlockDevice {
773 name: "sdb".to_string(),
774 maj_min: MajMin { major: 8, minor: 16 },
775 rm: false,
776 size: 536_870_912_000, ro: false,
778 device_type: DeviceType::Disk,
779 mountpoints: vec![None],
780 children: None,
781 };
782
783 let devices = BlockDevices {
785 blockdevices: vec![device1, device2],
786 };
787
788 let names: Vec<String> = devices.into_iter().map(|dev| dev.name).collect();
790 assert_eq!(names, vec!["sda".to_string(), "sdb".to_string()]);
791 }
792
793 #[test]
794 fn test_empty_blockdevices() {
795 let json = r#"{"blockdevices": []}"#;
796 let devices = parse_lsblk(json).expect("Failed to parse empty JSON");
797 assert!(devices.is_empty());
798 assert_eq!(devices.len(), 0);
799 assert!(devices.non_system().is_empty());
800 assert!(devices.system().is_empty());
801 assert!(devices.find_by_name("sda").is_none());
802 }
803
804 #[test]
805 fn test_default_trait() {
806 let devices = BlockDevices::default();
807 assert!(devices.is_empty());
808 assert_eq!(devices.len(), 0);
809 }
810
811 #[test]
812 fn test_clone_trait() {
813 let json = r#"{"blockdevices": [{"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]}]}"#;
814 let devices = parse_lsblk(json).expect("Failed to parse JSON");
815 let cloned = devices.clone();
816 assert_eq!(devices, cloned);
817 assert_eq!(cloned.len(), 1);
818 }
819
820 #[test]
821 fn test_serialization_roundtrip() {
822 let json = r#"{"blockdevices":[{"name":"sda","maj:min":"8:0","rm":false,"size":"500G","ro":false,"type":"disk","mountpoints":[null],"children":null}]}"#;
823 let devices = parse_lsblk(json).expect("Failed to parse JSON");
824 let serialized = serde_json::to_string(&devices).expect("Failed to serialize");
825 let deserialized: BlockDevices =
826 serde_json::from_str(&serialized).expect("Failed to deserialize");
827 assert_eq!(devices, deserialized);
828 }
829
830 #[test]
831 fn test_device_with_direct_root_mount() {
832 let json = r#"{
833 "blockdevices": [{
834 "name": "sda",
835 "maj:min": "8:0",
836 "rm": false,
837 "size": "500G",
838 "ro": false,
839 "type": "disk",
840 "mountpoints": ["/"]
841 }]
842 }"#;
843 let devices = parse_lsblk(json).expect("Failed to parse JSON");
844 let device = devices.find_by_name("sda").unwrap();
845 assert!(device.is_system());
846 assert!(device.is_mounted());
847 assert_eq!(device.active_mountpoints(), vec!["/"]);
848 assert_eq!(devices.system().len(), 1);
849 assert!(devices.non_system().is_empty());
850 }
851
852 #[test]
853 fn test_block_device_methods() {
854 let device = BlockDevice {
855 name: "sda".to_string(),
856 maj_min: MajMin { major: 8, minor: 0 },
857 rm: false,
858 size: 536_870_912_000, ro: false,
860 device_type: DeviceType::Disk,
861 mountpoints: vec![Some("/mnt/data".to_string()), None],
862 children: Some(vec![BlockDevice {
863 name: "sda1".to_string(),
864 maj_min: MajMin { major: 8, minor: 1 },
865 rm: false,
866 size: 268_435_456_000, ro: false,
868 device_type: DeviceType::Part,
869 mountpoints: vec![Some("/home".to_string())],
870 children: None,
871 }]),
872 };
873
874 assert!(device.is_disk());
875 assert!(!device.is_partition());
876 assert!(device.has_children());
877 assert!(device.is_mounted());
878 assert_eq!(device.active_mountpoints(), vec!["/mnt/data"]);
879
880 let child = device.find_child("sda1").unwrap();
881 assert!(!child.is_disk());
882 assert!(child.is_partition());
883 assert!(!child.has_children());
884
885 assert!(device.find_child("nonexistent").is_none());
886 }
887
888 #[test]
889 fn test_children_iter() {
890 let device = BlockDevice {
891 name: "sda".to_string(),
892 maj_min: MajMin { major: 8, minor: 0 },
893 rm: false,
894 size: 536_870_912_000, ro: false,
896 device_type: DeviceType::Disk,
897 mountpoints: vec![None],
898 children: Some(vec![
899 BlockDevice {
900 name: "sda1".to_string(),
901 maj_min: MajMin { major: 8, minor: 1 },
902 rm: false,
903 size: 268_435_456_000, ro: false,
905 device_type: DeviceType::Part,
906 mountpoints: vec![None],
907 children: None,
908 },
909 BlockDevice {
910 name: "sda2".to_string(),
911 maj_min: MajMin { major: 8, minor: 2 },
912 rm: false,
913 size: 268_435_456_000, ro: false,
915 device_type: DeviceType::Part,
916 mountpoints: vec![None],
917 children: None,
918 },
919 ]),
920 };
921
922 let names: Vec<&str> = device.children_iter().map(|c| c.name.as_str()).collect();
923 assert_eq!(names, vec!["sda1", "sda2"]);
924
925 let device_no_children = BlockDevice {
927 name: "sdb".to_string(),
928 maj_min: MajMin { major: 8, minor: 16 },
929 rm: false,
930 size: 536_870_912_000, ro: false,
932 device_type: DeviceType::Disk,
933 mountpoints: vec![None],
934 children: None,
935 };
936 assert_eq!(device_no_children.children_iter().count(), 0);
937 }
938
939 #[test]
940 fn test_borrowing_iterator() {
941 let devices = BlockDevices {
942 blockdevices: vec![
943 BlockDevice {
944 name: "sda".to_string(),
945 maj_min: MajMin { major: 8, minor: 0 },
946 rm: false,
947 size: 536_870_912_000, ro: false,
949 device_type: DeviceType::Disk,
950 mountpoints: vec![None],
951 children: None,
952 },
953 BlockDevice {
954 name: "sdb".to_string(),
955 maj_min: MajMin { major: 8, minor: 16 },
956 rm: false,
957 size: 536_870_912_000, ro: false,
959 device_type: DeviceType::Disk,
960 mountpoints: vec![None],
961 children: None,
962 },
963 ],
964 };
965
966 let names: Vec<&str> = (&devices).into_iter().map(|d| d.name.as_str()).collect();
968 assert_eq!(names, vec!["sda", "sdb"]);
969
970 assert_eq!(devices.len(), 2);
972
973 let names2: Vec<&str> = devices.iter().map(|d| d.name.as_str()).collect();
975 assert_eq!(names2, vec!["sda", "sdb"]);
976 }
977
978 #[test]
979 fn test_find_by_name() {
980 let devices = BlockDevices {
981 blockdevices: vec![
982 BlockDevice {
983 name: "sda".to_string(),
984 maj_min: MajMin { major: 8, minor: 0 },
985 rm: false,
986 size: 536_870_912_000, ro: false,
988 device_type: DeviceType::Disk,
989 mountpoints: vec![None],
990 children: None,
991 },
992 BlockDevice {
993 name: "nvme0n1".to_string(),
994 maj_min: MajMin { major: 259, minor: 0 },
995 rm: false,
996 size: 1_099_511_627_776, ro: false,
998 device_type: DeviceType::Disk,
999 mountpoints: vec![None],
1000 children: None,
1001 },
1002 ],
1003 };
1004
1005 assert!(devices.find_by_name("sda").is_some());
1006 assert_eq!(devices.find_by_name("sda").unwrap().size, 536_870_912_000);
1007 assert!(devices.find_by_name("nvme0n1").is_some());
1008 assert!(devices.find_by_name("nonexistent").is_none());
1009 }
1010
1011 #[test]
1012 fn test_system_method() {
1013 let json = r#"{
1014 "blockdevices": [
1015 {"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": ["/"]},
1016 {"name": "sdb", "maj:min": "8:16", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]},
1017 {"name": "sdc", "maj:min": "8:32", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": ["/home"]}
1018 ]
1019 }"#;
1020 let devices = parse_lsblk(json).expect("Failed to parse JSON");
1021 let system = devices.system();
1022 assert_eq!(system.len(), 1);
1023 assert_eq!(system[0].name, "sda");
1024 }
1025
1026 #[test]
1027 fn test_multiple_mountpoints() {
1028 let json = r#"{
1029 "blockdevices": [{
1030 "name": "sda",
1031 "maj:min": "8:0",
1032 "rm": false,
1033 "size": "500G",
1034 "ro": false,
1035 "type": "disk",
1036 "mountpoints": ["/mnt/data", "/mnt/backup", null]
1037 }]
1038 }"#;
1039 let devices = parse_lsblk(json).expect("Failed to parse JSON");
1040 let device = devices.find_by_name("sda").unwrap();
1041 assert!(device.is_mounted());
1042 assert_eq!(
1043 device.active_mountpoints(),
1044 vec!["/mnt/data", "/mnt/backup"]
1045 );
1046 }
1047
1048 #[test]
1049 fn test_removable_and_readonly() {
1050 let json = r#"{
1051 "blockdevices": [{
1052 "name": "sr0",
1053 "maj:min": "11:0",
1054 "rm": true,
1055 "size": "4.7G",
1056 "ro": true,
1057 "type": "rom",
1058 "mountpoints": [null]
1059 }]
1060 }"#;
1061 let devices = parse_lsblk(json).expect("Failed to parse JSON");
1062 let device = devices.find_by_name("sr0").unwrap();
1063 assert!(device.rm);
1064 assert!(device.ro);
1065 assert_eq!(device.device_type, DeviceType::Rom);
1066 }
1067}