blockdev/
lib.rs

1use serde::de::Error as DeError;
2use serde::{Deserialize, Deserializer, Serialize};
3use serde_json::Value;
4use std::error::Error;
5use std::process::Command;
6use std::slice::Iter;
7use std::vec::IntoIter;
8
9/// Represents the entire JSON output produced by `lsblk --json`.
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
11pub struct BlockDevices {
12    /// A vector of block devices.
13    pub blockdevices: Vec<BlockDevice>,
14}
15
16/// Custom deserializer that supports both a single mountpoint (which may be null)
17/// and an array of mountpoints.
18///
19/// # Arguments
20///
21/// * `deserializer` - The deserializer instance.
22///
23/// # Returns
24///
25/// A vector of optional strings representing mountpoints.
26///
27/// # Errors
28///
29/// Returns an error if the value cannot be deserialized either as a single value or as an array.
30///
31/// This function is used internally by Serde when deserializing block devices.
32/// For example, if the JSON value is `null`, it will be converted to `vec![None]`.
33fn deserialize_mountpoints<'de, D>(deserializer: D) -> Result<Vec<Option<String>>, D::Error>
34where
35    D: Deserializer<'de>,
36{
37    let value = Value::deserialize(deserializer)?;
38    if value.is_array() {
39        // Deserialize as an array of optional strings.
40        serde_json::from_value(value).map_err(DeError::custom)
41    } else {
42        // Otherwise, deserialize as a single Option<String> and wrap it in a vector.
43        let single: Option<String> = serde_json::from_value(value).map_err(DeError::custom)?;
44        Ok(vec![single])
45    }
46}
47
48/// Represents a block device as output by `lsblk`.
49///
50/// Note that the `children` field is optional, as some devices might not have any nested children.
51///
52/// # Field Details
53///
54/// - `name`: The device name.
55/// - `maj_min`: The device's major and minor numbers. (Renamed from the JSON field "maj:min")
56/// - `rm`: Whether the device is removable.
57/// - `size`: The device size.
58/// - `ro`: Whether the device is read-only.
59/// - `device_type`: The device type (renamed from the reserved keyword "type").
60/// - `mountpoints`: A vector of mountpoints for the device. Uses a custom deserializer to support both single and multiple mountpoints.
61/// - `children`: Optional nested block devices.
62#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
63pub struct BlockDevice {
64    /// The name of the block device.
65    pub name: String,
66    /// The major and minor numbers of the block device.
67    ///
68    /// This field corresponds to the JSON field `"maj:min"`.
69    #[serde(rename = "maj:min")]
70    pub maj_min: String,
71    /// Indicates if the device is removable.
72    pub rm: bool,
73    /// The size of the block device.
74    pub size: String,
75    /// Indicates if the device is read-only.
76    pub ro: bool,
77    /// The type of the block device.
78    ///
79    /// The JSON field is `"type"`, which is a reserved keyword in Rust. It is renamed to `device_type`.
80    #[serde(rename = "type")]
81    pub device_type: String,
82    /// The mountpoints of the device.
83    ///
84    /// Uses a custom deserializer to handle both a single mountpoint (possibly null) and an array of mountpoints.
85    #[serde(
86        default,
87        alias = "mountpoint",
88        deserialize_with = "deserialize_mountpoints"
89    )]
90    pub mountpoints: Vec<Option<String>>,
91    /// Optional nested children block devices.
92    #[serde(default)]
93    pub children: Option<Vec<BlockDevice>>,
94}
95
96impl BlockDevice {
97    /// Returns `true` if this device has any children.
98    #[must_use]
99    pub fn has_children(&self) -> bool {
100        self.children.as_ref().is_some_and(|c| !c.is_empty())
101    }
102
103    /// Returns an iterator over the children of this device.
104    ///
105    /// Returns an empty iterator if the device has no children.
106    pub fn children_iter(&self) -> impl Iterator<Item = &BlockDevice> {
107        self.children.iter().flat_map(|c| c.iter())
108    }
109
110    /// Finds a direct child device by name.
111    ///
112    /// Returns `None` if no child with the given name exists.
113    #[must_use]
114    pub fn find_child(&self, name: &str) -> Option<&BlockDevice> {
115        self.children.as_ref()?.iter().find(|c| c.name == name)
116    }
117
118    /// Returns all non-null mountpoints for this device.
119    #[must_use]
120    pub fn active_mountpoints(&self) -> Vec<&str> {
121        self.mountpoints
122            .iter()
123            .filter_map(|m| m.as_deref())
124            .collect()
125    }
126
127    /// Returns `true` if this device has at least one mountpoint.
128    #[must_use]
129    pub fn is_mounted(&self) -> bool {
130        self.mountpoints.iter().any(|m| m.is_some())
131    }
132
133    /// Determines if this block device or any of its recursive children has a mountpoint of `/`,
134    /// indicating a system mount.
135    #[must_use]
136    pub fn is_system(&self) -> bool {
137        if self.mountpoints.iter().any(|m| m.as_deref() == Some("/")) {
138            return true;
139        }
140        if let Some(children) = &self.children {
141            for child in children {
142                if child.is_system() {
143                    return true;
144                }
145            }
146        }
147        false
148    }
149
150    /// Returns `true` if this device is a disk (device_type == "disk").
151    #[must_use]
152    pub fn is_disk(&self) -> bool {
153        self.device_type == "disk"
154    }
155
156    /// Returns `true` if this device is a partition (device_type == "part").
157    #[must_use]
158    pub fn is_partition(&self) -> bool {
159        self.device_type == "part"
160    }
161}
162
163impl BlockDevices {
164    /// Returns the number of top-level block devices.
165    #[must_use]
166    pub fn len(&self) -> usize {
167        self.blockdevices.len()
168    }
169
170    /// Returns `true` if there are no block devices.
171    #[must_use]
172    pub fn is_empty(&self) -> bool {
173        self.blockdevices.is_empty()
174    }
175
176    /// Returns an iterator over references to the block devices.
177    pub fn iter(&self) -> Iter<'_, BlockDevice> {
178        self.blockdevices.iter()
179    }
180
181    /// Returns a vector of references to `BlockDevice` entries that have a mountpoint
182    /// of `/` on them or on any of their recursive children.
183    #[must_use]
184    pub fn system(&self) -> Vec<&BlockDevice> {
185        self.blockdevices
186            .iter()
187            .filter(|device| device.is_system())
188            .collect()
189    }
190
191    /// Returns a vector of references to `BlockDevice` entries that do not have a mountpoint
192    /// of `/` on them or on any of their recursive children.
193    #[must_use]
194    pub fn non_system(&self) -> Vec<&BlockDevice> {
195        self.blockdevices
196            .iter()
197            .filter(|device| !device.is_system())
198            .collect()
199    }
200
201    /// Finds a top-level block device by name.
202    ///
203    /// Returns `None` if no device with the given name exists.
204    #[must_use]
205    pub fn find_by_name(&self, name: &str) -> Option<&BlockDevice> {
206        self.blockdevices.iter().find(|d| d.name == name)
207    }
208}
209
210impl IntoIterator for BlockDevices {
211    type Item = BlockDevice;
212    type IntoIter = IntoIter<BlockDevice>;
213
214    fn into_iter(self) -> Self::IntoIter {
215        self.blockdevices.into_iter()
216    }
217}
218
219impl<'a> IntoIterator for &'a BlockDevices {
220    type Item = &'a BlockDevice;
221    type IntoIter = Iter<'a, BlockDevice>;
222
223    fn into_iter(self) -> Self::IntoIter {
224        self.blockdevices.iter()
225    }
226}
227
228/// Parses a JSON string (produced by `lsblk --json`)
229/// into a `BlockDevices` struct.
230///
231/// This function is useful when you already have JSON data from `lsblk`
232/// and want to parse it without running the command again.
233///
234/// # Arguments
235///
236/// * `json_data` - A string slice containing the JSON data.
237///
238/// # Errors
239///
240/// Returns a `serde_json::Error` if the JSON cannot be parsed.
241///
242/// # Examples
243///
244/// ```
245/// use blockdev::parse_lsblk;
246///
247/// let json = r#"{"blockdevices": [{"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]}]}"#;
248/// let devices = parse_lsblk(json).expect("Failed to parse JSON");
249/// assert_eq!(devices.len(), 1);
250/// ```
251pub fn parse_lsblk(json_data: &str) -> Result<BlockDevices, serde_json::Error> {
252    serde_json::from_str(json_data)
253}
254
255/// Runs the `lsblk --json` command, captures its output, and parses it
256/// into a `BlockDevices` struct. If the command fails or the output cannot be parsed,
257/// an error is returned.
258///
259/// # Errors
260///
261/// Returns an error if the `lsblk` command fails or if the output cannot be parsed as valid JSON.
262///
263/// # Examples
264///
265/// ```no_run
266/// # use blockdev::get_devices;
267/// let devices = get_devices().expect("Failed to get block devices");
268/// ```
269pub fn get_devices() -> Result<BlockDevices, Box<dyn Error>> {
270    let output = Command::new("lsblk").arg("--json").output()?;
271
272    if !output.status.success() {
273        return Err(format!("lsblk failed: {}", String::from_utf8_lossy(&output.stderr)).into());
274    }
275
276    let json_output = String::from_utf8(output.stdout)?;
277    let lsblk = parse_lsblk(&json_output)?;
278    Ok(lsblk)
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    const SAMPLE_JSON: &str = r#"
286    {
287        "blockdevices": [
288            {"name":"nvme1n1", "maj:min":"259:0", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
289                "children": [
290                    {"name":"nvme1n1p1", "maj:min":"259:1", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
291                    {"name":"nvme1n1p9", "maj:min":"259:2", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
292                ]
293            },
294            {"name":"nvme7n1", "maj:min":"259:3", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
295                "children": [
296                    {"name":"nvme7n1p1", "maj:min":"259:7", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
297                    {"name":"nvme7n1p9", "maj:min":"259:8", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
298                ]
299            },
300            {"name":"nvme5n1", "maj:min":"259:4", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
301                "children": [
302                    {"name":"nvme5n1p1", "maj:min":"259:5", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
303                    {"name":"nvme5n1p9", "maj:min":"259:6", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
304                ]
305            },
306            {"name":"nvme9n1", "maj:min":"259:9", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
307                "children": [
308                    {"name":"nvme9n1p1", "maj:min":"259:13", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
309                    {"name":"nvme9n1p9", "maj:min":"259:14", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
310                ]
311            },
312            {"name":"nvme4n1", "maj:min":"259:10", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
313                "children": [
314                    {"name":"nvme4n1p1", "maj:min":"259:11", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
315                    {"name":"nvme4n1p9", "maj:min":"259:12", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
316                ]
317            },
318            {"name":"nvme8n1", "maj:min":"259:15", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
319                "children": [
320                    {"name":"nvme8n1p1", "maj:min":"259:20", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
321                    {"name":"nvme8n1p9", "maj:min":"259:21", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
322                ]
323            },
324            {"name":"nvme6n1", "maj:min":"259:16", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
325                "children": [
326                    {"name":"nvme6n1p1", "maj:min":"259:17", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
327                    {"name":"nvme6n1p9", "maj:min":"259:18", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
328                ]
329            },
330            {"name":"nvme3n1", "maj:min":"259:19", "rm":false, "size":"894.3G", "ro":false, "type":"disk", "mountpoint":null,
331                "children": [
332                    {"name":"nvme3n1p1", "maj:min":"259:23", "rm":false, "size":"1M", "ro":false, "type":"part", "mountpoint":null},
333                    {"name":"nvme3n1p2", "maj:min":"259:24", "rm":false, "size":"244M", "ro":false, "type":"part", "mountpoint":"/boot/efi"},
334                    {"name":"nvme3n1p3", "maj:min":"259:25", "rm":false, "size":"488M", "ro":false, "type":"part", "mountpoint":null,
335                    "children": [
336                        {"name":"md0", "maj:min":"9:0", "rm":false, "size":"487M", "ro":false, "type":"raid1", "mountpoint":"/boot"}
337                    ]
338                    },
339                    {"name":"nvme3n1p4", "maj:min":"259:26", "rm":false, "size":"7.6G", "ro":false, "type":"part", "mountpoint":null,
340                    "children": [
341                        {"name":"md1", "maj:min":"9:1", "rm":false, "size":"7.6G", "ro":false, "type":"raid1", "mountpoint":"[SWAP]"}
342                    ]
343                    },
344                    {"name":"nvme3n1p5", "maj:min":"259:27", "rm":false, "size":"19.1G", "ro":false, "type":"part", "mountpoint":null,
345                    "children": [
346                        {"name":"md2", "maj:min":"9:2", "rm":false, "size":"19.1G", "ro":false, "type":"raid1", "mountpoint":"/"}
347                    ]
348                    },
349                    {"name":"nvme3n1p6", "maj:min":"259:28", "rm":false, "size":"866.8G", "ro":false, "type":"part", "mountpoint":null}
350                ]
351            },
352            {"name":"nvme0n1", "maj:min":"259:22", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
353                "children": [
354                    {"name":"nvme0n1p1", "maj:min":"259:29", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
355                    {"name":"nvme0n1p9", "maj:min":"259:30", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
356                ]
357            },
358            {"name":"nvme2n1", "maj:min":"259:31", "rm":false, "size":"894.3G", "ro":false, "type":"disk", "mountpoint":null,
359                "children": [
360                    {"name":"nvme2n1p1", "maj:min":"259:32", "rm":false, "size":"1M", "ro":false, "type":"part", "mountpoint":null},
361                    {"name":"nvme2n1p2", "maj:min":"259:33", "rm":false, "size":"244M", "ro":false, "type":"part", "mountpoint":null},
362                    {"name":"nvme2n1p3", "maj:min":"259:34", "rm":false, "size":"488M", "ro":false, "type":"part", "mountpoint":null,
363                    "children": [
364                        {"name":"md0", "maj:min":"9:0", "rm":false, "size":"487M", "ro":false, "type":"raid1", "mountpoint":"/boot"}
365                    ]
366                    },
367                    {"name":"nvme2n1p4", "maj:min":"259:35", "rm":false, "size":"7.6G", "ro":false, "type":"part", "mountpoint":null,
368                    "children": [
369                        {"name":"md1", "maj:min":"9:1", "rm":false, "size":"7.6G", "ro":false, "type":"raid1", "mountpoint":"[SWAP]"}
370                    ]
371                    },
372                    {"name":"nvme2n1p5", "maj:min":"259:36", "rm":false, "size":"19.1G", "ro":false, "type":"part", "mountpoint":null,
373                    "children": [
374                        {"name":"md2", "maj:min":"9:2", "rm":false, "size":"19.1G", "ro":false, "type":"raid1", "mountpoint":"/"}
375                    ]
376                    },
377                    {"name":"nvme2n1p6", "maj:min":"259:37", "rm":false, "size":"866.8G", "ro":false, "type":"part", "mountpoint":null}
378                ]
379            }
380        ]
381    }
382    "#;
383
384    #[test]
385    fn test_parse_lsblk() {
386        let lsblk = parse_lsblk(SAMPLE_JSON).expect("Failed to parse JSON");
387
388        // Assert the expected number of top-level block devices.
389        assert_eq!(
390            lsblk.blockdevices.len(),
391            10,
392            "Expected 10 top-level block devices"
393        );
394
395        // Verify that required fields are non-empty.
396        for device in &lsblk.blockdevices {
397            assert!(!device.name.is_empty(), "Device name should not be empty");
398            assert!(
399                !device.maj_min.is_empty(),
400                "Device maj:min should not be empty"
401            );
402        }
403
404        // Pick a device with nested children and validate details.
405        let nvme3n1 = lsblk
406            .blockdevices
407            .iter()
408            .find(|d| d.name == "nvme3n1")
409            .expect("Expected to find device nvme3n1");
410
411        // Its first mountpoint should be None.
412        assert!(
413            nvme3n1
414                .mountpoints
415                .first()
416                .and_then(|opt| opt.as_deref())
417                .is_none(),
418            "nvme3n1 effective mountpoint should be None"
419        );
420
421        // Verify that nvme3n1 has exactly 6 children.
422        let children = nvme3n1
423            .children
424            .as_ref()
425            .expect("nvme3n1 should have children");
426        assert_eq!(children.len(), 6, "nvme3n1 should have 6 children");
427
428        // Validate that child nvme3n1p2 has first mountpoint of "/boot/efi".
429        let nvme3n1p2 = children
430            .iter()
431            .find(|c| c.name == "nvme3n1p2")
432            .expect("Expected to find nvme3n1p2");
433        assert_eq!(
434            nvme3n1p2.mountpoints.first().and_then(|opt| opt.as_deref()),
435            Some("/boot/efi"),
436            "nvme3n1p2 first mountpoint should be '/boot/efi'"
437        );
438
439        // In nvme3n1p3, verify that its nested child md0 has an effective mountpoint of "/boot".
440        let nvme3n1p3 = children
441            .iter()
442            .find(|c| c.name == "nvme3n1p3")
443            .expect("Expected to find nvme3n1p3");
444        let nested_children = nvme3n1p3
445            .children
446            .as_ref()
447            .expect("nvme3n1p3 should have children");
448        let md0 = nested_children
449            .iter()
450            .find(|d| d.name == "md0")
451            .expect("Expected to find md0 under nvme3n1p3");
452        assert_eq!(
453            md0.mountpoints.first().and_then(|opt| opt.as_deref()),
454            Some("/boot"),
455            "md0 effective mountpoint should be '/boot'"
456        );
457
458        // Test the non_system method.
459        // Since nvme3n1 has a descendant (md2) with effective mountpoint "/" it should be excluded.
460        let non_system = lsblk.non_system();
461        assert_eq!(
462            non_system.len(),
463            8,
464            "Expected 8 non-system top-level devices, since nvme3n1/nvme2n1 is system"
465        );
466        assert!(
467            !non_system.iter().any(|d| d.name == "nvme3n1"),
468            "nvme3n1 should be excluded from non-system devices"
469        );
470    }
471
472    #[test]
473    fn test_non_system() {
474        // Create a JSON where one device is system (has "/" mountpoint in a child)
475        // and one is non-system.
476        let test_json = r#"
477        {
478            "blockdevices": [
479                {
480                    "name": "sda",
481                    "maj:min": "8:0",
482                    "rm": false,
483                    "size": "447.1G",
484                    "ro": false,
485                    "type": "disk",
486                    "mountpoints": [
487                        null
488                    ],
489                    "children": [
490                        {
491                        "name": "sda1",
492                        "maj:min": "8:1",
493                        "rm": false,
494                        "size": "512M",
495                        "ro": false,
496                        "type": "part",
497                        "mountpoints": [
498                            null
499                        ]
500                        },{
501                        "name": "sda2",
502                        "maj:min": "8:2",
503                        "rm": false,
504                        "size": "446.6G",
505                        "ro": false,
506                        "type": "part",
507                        "mountpoints": [
508                            null
509                        ],
510                        "children": [
511                            {
512                                "name": "md0",
513                                "maj:min": "9:0",
514                                "rm": false,
515                                "size": "446.6G",
516                                "ro": false,
517                                "type": "raid1",
518                                "mountpoints": [
519                                    "/"
520                                ]
521                            }
522                        ]
523                        }
524                    ]
525                },{
526                    "name": "sdb",
527                    "maj:min": "8:16",
528                    "rm": false,
529                    "size": "447.1G",
530                    "ro": false,
531                    "type": "disk",
532                    "mountpoints": [
533                        null
534                    ],
535                    "children": [
536                        {
537                        "name": "sdb1",
538                        "maj:min": "8:17",
539                        "rm": false,
540                        "size": "512M",
541                        "ro": false,
542                        "type": "part",
543                        "mountpoints": [
544                            "/boot/efi"
545                        ]
546                        },{
547                        "name": "sdb2",
548                        "maj:min": "8:18",
549                        "rm": false,
550                        "size": "446.6G",
551                        "ro": false,
552                        "type": "part",
553                        "mountpoints": [
554                            null
555                        ],
556                        "children": [
557                            {
558                                "name": "md0",
559                                "maj:min": "9:0",
560                                "rm": false,
561                                "size": "446.6G",
562                                "ro": false,
563                                "type": "raid1",
564                                "mountpoints": [
565                                    "/"
566                                ]
567                            }
568                        ]
569                        }
570                    ]
571                },{
572                    "name": "nvme0n1",
573                    "maj:min": "259:2",
574                    "rm": false,
575                    "size": "1.7T",
576                    "ro": false,
577                    "type": "disk",
578                    "mountpoints": [
579                        null
580                    ]
581                },{
582                    "name": "nvme1n1",
583                    "maj:min": "259:3",
584                    "rm": false,
585                    "size": "1.7T",
586                    "ro": false,
587                    "type": "disk",
588                    "mountpoints": [
589                        null
590                    ]
591                }
592            ]
593        }
594        "#;
595        let disks = parse_lsblk(test_json).unwrap();
596        let non_system = disks.non_system();
597        assert_eq!(non_system.len(), 2);
598        let names: Vec<&str> = non_system.iter().map(|d| d.name.as_str()).collect();
599        assert_eq!(names, vec!["nvme0n1", "nvme1n1"]);
600    }
601
602    /// Warning: This test will attempt to run the `lsblk` command on your system.
603    /// It may fail if `lsblk` is not available or if the test environment does not permit running commands.
604    #[test]
605    #[ignore = "requires lsblk command to be available on the system"]
606    fn test_get_devices() {
607        let dev = get_devices().expect("Failed to get block devices");
608        // This assertion is simplistic; adjust according to your environment's expected output.
609        assert!(!dev.blockdevices.is_empty());
610    }
611    #[test]
612    fn test_into_iterator() {
613        // Create dummy BlockDevice instances.
614        let device1 = BlockDevice {
615            name: "sda".to_string(),
616            maj_min: "8:0".to_string(),
617            rm: false,
618            size: "500G".to_string(),
619            ro: false,
620            device_type: "disk".to_string(),
621            mountpoints: vec![None],
622            children: None,
623        };
624
625        let device2 = BlockDevice {
626            name: "sdb".to_string(),
627            maj_min: "8:16".to_string(),
628            rm: false,
629            size: "500G".to_string(),
630            ro: false,
631            device_type: "disk".to_string(),
632            mountpoints: vec![None],
633            children: None,
634        };
635
636        // Create a BlockDevices instance containing the two devices.
637        let devices = BlockDevices {
638            blockdevices: vec![device1, device2],
639        };
640
641        // Use the IntoIterator implementation to iterate over the devices.
642        let names: Vec<String> = devices.into_iter().map(|dev| dev.name).collect();
643        assert_eq!(names, vec!["sda".to_string(), "sdb".to_string()]);
644    }
645
646    #[test]
647    fn test_empty_blockdevices() {
648        let json = r#"{"blockdevices": []}"#;
649        let devices = parse_lsblk(json).expect("Failed to parse empty JSON");
650        assert!(devices.is_empty());
651        assert_eq!(devices.len(), 0);
652        assert!(devices.non_system().is_empty());
653        assert!(devices.system().is_empty());
654        assert!(devices.find_by_name("sda").is_none());
655    }
656
657    #[test]
658    fn test_default_trait() {
659        let devices = BlockDevices::default();
660        assert!(devices.is_empty());
661        assert_eq!(devices.len(), 0);
662    }
663
664    #[test]
665    fn test_clone_trait() {
666        let json = r#"{"blockdevices": [{"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]}]}"#;
667        let devices = parse_lsblk(json).expect("Failed to parse JSON");
668        let cloned = devices.clone();
669        assert_eq!(devices, cloned);
670        assert_eq!(cloned.len(), 1);
671    }
672
673    #[test]
674    fn test_serialization_roundtrip() {
675        let json = r#"{"blockdevices":[{"name":"sda","maj:min":"8:0","rm":false,"size":"500G","ro":false,"type":"disk","mountpoints":[null],"children":null}]}"#;
676        let devices = parse_lsblk(json).expect("Failed to parse JSON");
677        let serialized = serde_json::to_string(&devices).expect("Failed to serialize");
678        let deserialized: BlockDevices =
679            serde_json::from_str(&serialized).expect("Failed to deserialize");
680        assert_eq!(devices, deserialized);
681    }
682
683    #[test]
684    fn test_device_with_direct_root_mount() {
685        let json = r#"{
686            "blockdevices": [{
687                "name": "sda",
688                "maj:min": "8:0",
689                "rm": false,
690                "size": "500G",
691                "ro": false,
692                "type": "disk",
693                "mountpoints": ["/"]
694            }]
695        }"#;
696        let devices = parse_lsblk(json).expect("Failed to parse JSON");
697        let device = devices.find_by_name("sda").unwrap();
698        assert!(device.is_system());
699        assert!(device.is_mounted());
700        assert_eq!(device.active_mountpoints(), vec!["/"]);
701        assert_eq!(devices.system().len(), 1);
702        assert!(devices.non_system().is_empty());
703    }
704
705    #[test]
706    fn test_block_device_methods() {
707        let device = BlockDevice {
708            name: "sda".to_string(),
709            maj_min: "8:0".to_string(),
710            rm: false,
711            size: "500G".to_string(),
712            ro: false,
713            device_type: "disk".to_string(),
714            mountpoints: vec![Some("/mnt/data".to_string()), None],
715            children: Some(vec![BlockDevice {
716                name: "sda1".to_string(),
717                maj_min: "8:1".to_string(),
718                rm: false,
719                size: "250G".to_string(),
720                ro: false,
721                device_type: "part".to_string(),
722                mountpoints: vec![Some("/home".to_string())],
723                children: None,
724            }]),
725        };
726
727        assert!(device.is_disk());
728        assert!(!device.is_partition());
729        assert!(device.has_children());
730        assert!(device.is_mounted());
731        assert_eq!(device.active_mountpoints(), vec!["/mnt/data"]);
732
733        let child = device.find_child("sda1").unwrap();
734        assert!(!child.is_disk());
735        assert!(child.is_partition());
736        assert!(!child.has_children());
737
738        assert!(device.find_child("nonexistent").is_none());
739    }
740
741    #[test]
742    fn test_children_iter() {
743        let device = BlockDevice {
744            name: "sda".to_string(),
745            maj_min: "8:0".to_string(),
746            rm: false,
747            size: "500G".to_string(),
748            ro: false,
749            device_type: "disk".to_string(),
750            mountpoints: vec![None],
751            children: Some(vec![
752                BlockDevice {
753                    name: "sda1".to_string(),
754                    maj_min: "8:1".to_string(),
755                    rm: false,
756                    size: "250G".to_string(),
757                    ro: false,
758                    device_type: "part".to_string(),
759                    mountpoints: vec![None],
760                    children: None,
761                },
762                BlockDevice {
763                    name: "sda2".to_string(),
764                    maj_min: "8:2".to_string(),
765                    rm: false,
766                    size: "250G".to_string(),
767                    ro: false,
768                    device_type: "part".to_string(),
769                    mountpoints: vec![None],
770                    children: None,
771                },
772            ]),
773        };
774
775        let names: Vec<&str> = device.children_iter().map(|c| c.name.as_str()).collect();
776        assert_eq!(names, vec!["sda1", "sda2"]);
777
778        // Test empty children iterator
779        let device_no_children = BlockDevice {
780            name: "sdb".to_string(),
781            maj_min: "8:16".to_string(),
782            rm: false,
783            size: "500G".to_string(),
784            ro: false,
785            device_type: "disk".to_string(),
786            mountpoints: vec![None],
787            children: None,
788        };
789        assert_eq!(device_no_children.children_iter().count(), 0);
790    }
791
792    #[test]
793    fn test_borrowing_iterator() {
794        let devices = BlockDevices {
795            blockdevices: vec![
796                BlockDevice {
797                    name: "sda".to_string(),
798                    maj_min: "8:0".to_string(),
799                    rm: false,
800                    size: "500G".to_string(),
801                    ro: false,
802                    device_type: "disk".to_string(),
803                    mountpoints: vec![None],
804                    children: None,
805                },
806                BlockDevice {
807                    name: "sdb".to_string(),
808                    maj_min: "8:16".to_string(),
809                    rm: false,
810                    size: "500G".to_string(),
811                    ro: false,
812                    device_type: "disk".to_string(),
813                    mountpoints: vec![None],
814                    children: None,
815                },
816            ],
817        };
818
819        // Test borrowing iterator (doesn't consume)
820        let names: Vec<&str> = (&devices).into_iter().map(|d| d.name.as_str()).collect();
821        assert_eq!(names, vec!["sda", "sdb"]);
822
823        // devices is still available
824        assert_eq!(devices.len(), 2);
825
826        // Test iter() method
827        let names2: Vec<&str> = devices.iter().map(|d| d.name.as_str()).collect();
828        assert_eq!(names2, vec!["sda", "sdb"]);
829    }
830
831    #[test]
832    fn test_find_by_name() {
833        let devices = BlockDevices {
834            blockdevices: vec![
835                BlockDevice {
836                    name: "sda".to_string(),
837                    maj_min: "8:0".to_string(),
838                    rm: false,
839                    size: "500G".to_string(),
840                    ro: false,
841                    device_type: "disk".to_string(),
842                    mountpoints: vec![None],
843                    children: None,
844                },
845                BlockDevice {
846                    name: "nvme0n1".to_string(),
847                    maj_min: "259:0".to_string(),
848                    rm: false,
849                    size: "1T".to_string(),
850                    ro: false,
851                    device_type: "disk".to_string(),
852                    mountpoints: vec![None],
853                    children: None,
854                },
855            ],
856        };
857
858        assert!(devices.find_by_name("sda").is_some());
859        assert_eq!(devices.find_by_name("sda").unwrap().size, "500G");
860        assert!(devices.find_by_name("nvme0n1").is_some());
861        assert!(devices.find_by_name("nonexistent").is_none());
862    }
863
864    #[test]
865    fn test_system_method() {
866        let json = r#"{
867            "blockdevices": [
868                {"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": ["/"]},
869                {"name": "sdb", "maj:min": "8:16", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]},
870                {"name": "sdc", "maj:min": "8:32", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": ["/home"]}
871            ]
872        }"#;
873        let devices = parse_lsblk(json).expect("Failed to parse JSON");
874        let system = devices.system();
875        assert_eq!(system.len(), 1);
876        assert_eq!(system[0].name, "sda");
877    }
878
879    #[test]
880    fn test_multiple_mountpoints() {
881        let json = r#"{
882            "blockdevices": [{
883                "name": "sda",
884                "maj:min": "8:0",
885                "rm": false,
886                "size": "500G",
887                "ro": false,
888                "type": "disk",
889                "mountpoints": ["/mnt/data", "/mnt/backup", null]
890            }]
891        }"#;
892        let devices = parse_lsblk(json).expect("Failed to parse JSON");
893        let device = devices.find_by_name("sda").unwrap();
894        assert!(device.is_mounted());
895        assert_eq!(
896            device.active_mountpoints(),
897            vec!["/mnt/data", "/mnt/backup"]
898        );
899    }
900
901    #[test]
902    fn test_removable_and_readonly() {
903        let json = r#"{
904            "blockdevices": [{
905                "name": "sr0",
906                "maj:min": "11:0",
907                "rm": true,
908                "size": "4.7G",
909                "ro": true,
910                "type": "rom",
911                "mountpoints": [null]
912            }]
913        }"#;
914        let devices = parse_lsblk(json).expect("Failed to parse JSON");
915        let device = devices.find_by_name("sr0").unwrap();
916        assert!(device.rm);
917        assert!(device.ro);
918        assert_eq!(device.device_type, "rom");
919    }
920}