xref: /DADK/dadk/src/actions/rootfs/loopdev.rs (revision a013ae121c339d37b6fa5273770148fa161c730b)
1 use core::str;
2 use std::{path::PathBuf, process::Command};
3 
4 use anyhow::{anyhow, Result};
5 use regex::Regex;
6 
7 use crate::utils::abs_path;
8 
9 const LOOP_DEVICE_LOSETUP_A_REGEX: &str = r"^/dev/loop(\d+)";
10 
11 pub struct LoopDevice {
12     img_path: Option<PathBuf>,
13     loop_device_path: Option<String>,
14 }
15 impl LoopDevice {
16     pub fn attached(&self) -> bool {
17         self.loop_device_path.is_some()
18     }
19 
20     pub fn dev_path(&self) -> Option<&String> {
21         self.loop_device_path.as_ref()
22     }
23 
24     pub fn attach(&mut self) -> Result<()> {
25         if self.attached() {
26             return Ok(());
27         }
28         if self.img_path.is_none() {
29             return Err(anyhow!("Image path not set"));
30         }
31 
32         let output = Command::new("losetup")
33             .arg("-f")
34             .arg("--show")
35             .arg("-P")
36             .arg(self.img_path.as_ref().unwrap())
37             .output()?;
38 
39         if output.status.success() {
40             let loop_device = String::from_utf8(output.stdout)?.trim().to_string();
41             self.loop_device_path = Some(loop_device);
42             log::trace!(
43                 "Loop device attached: {}",
44                 self.loop_device_path.as_ref().unwrap()
45             );
46             Ok(())
47         } else {
48             Err(anyhow::anyhow!(
49                 "Failed to mount disk image: losetup command exited with status {}",
50                 output.status
51             ))
52         }
53     }
54 
55     /// 尝试连接已经存在的loop device
56     pub fn attach_by_exists(&mut self) -> Result<()> {
57         if self.attached() {
58             return Ok(());
59         }
60         if self.img_path.is_none() {
61             return Err(anyhow!("Image path not set"));
62         }
63         log::trace!(
64             "Try to attach loop device by exists: image path: {}",
65             self.img_path.as_ref().unwrap().display()
66         );
67         // losetup -a 查看是否有已经attach了的,如果有,就附着上去
68         let cmd = Command::new("losetup")
69             .arg("-a")
70             .output()
71             .map_err(|e| anyhow!("Failed to run losetup -a: {}", e))?;
72         let output = String::from_utf8(cmd.stdout)?;
73         let s = __loop_device_path_by_disk_image_path(
74             self.img_path.as_ref().unwrap().to_str().unwrap(),
75             &output,
76         )
77         .map_err(|e| anyhow!("Failed to find loop device: {}", e))?;
78         self.loop_device_path = Some(s);
79         Ok(())
80     }
81 
82     /// 获取指定分区的路径
83     ///
84     /// # 参数
85     ///
86     /// * `nth` - 分区的编号
87     ///
88     /// # 返回值
89     ///
90     /// 返回一个 `Result<String>`,包含分区路径的字符串。如果循环设备未附加,则返回错误。
91     ///
92     /// # 错误
93     ///
94     /// 如果循环设备未附加,则返回 `anyhow!("Loop device not attached")` 错误。
95     pub fn partition_path(&self, nth: u8) -> Result<PathBuf> {
96         if !self.attached() {
97             return Err(anyhow!("Loop device not attached"));
98         }
99         let s = format!("{}p{}", self.loop_device_path.as_ref().unwrap(), nth);
100         let s = PathBuf::from(s);
101         // 判断路径是否存在
102         if !s.exists() {
103             return Err(anyhow!("Partition not exist"));
104         }
105         Ok(s)
106     }
107 
108     pub fn detach(&mut self) -> Result<()> {
109         if self.loop_device_path.is_none() {
110             return Ok(());
111         }
112         let loop_device = self.loop_device_path.take().unwrap();
113         let output = Command::new("losetup")
114             .arg("-d")
115             .arg(loop_device)
116             .output()?;
117 
118         if output.status.success() {
119             self.loop_device_path = None;
120             Ok(())
121         } else {
122             Err(anyhow::anyhow!(
123                 "Failed to detach loop device: {}, {}",
124                 output.status,
125                 str::from_utf8(output.stderr.as_slice()).unwrap_or("<Unknown>")
126             ))
127         }
128     }
129 }
130 
131 impl Drop for LoopDevice {
132     fn drop(&mut self) {
133         if let Err(e) = self.detach() {
134             log::warn!("Failed to detach loop device: {}", e);
135         }
136     }
137 }
138 
139 pub struct LoopDeviceBuilder {
140     img_path: Option<PathBuf>,
141     loop_device_path: Option<String>,
142 }
143 
144 impl LoopDeviceBuilder {
145     pub fn new() -> Self {
146         LoopDeviceBuilder {
147             img_path: None,
148             loop_device_path: None,
149         }
150     }
151 
152     pub fn img_path(mut self, img_path: PathBuf) -> Self {
153         self.img_path = Some(abs_path(&img_path));
154         self
155     }
156 
157     pub fn build(self) -> Result<LoopDevice> {
158         let loop_dev = LoopDevice {
159             img_path: self.img_path,
160             loop_device_path: self.loop_device_path,
161         };
162 
163         Ok(loop_dev)
164     }
165 }
166 
167 fn __loop_device_path_by_disk_image_path(
168     disk_img_path: &str,
169     losetup_a_output: &str,
170 ) -> Result<String> {
171     let re = Regex::new(LOOP_DEVICE_LOSETUP_A_REGEX)?;
172     for line in losetup_a_output.lines() {
173         if !line.contains(disk_img_path) {
174             continue;
175         }
176         let caps = re.captures(line);
177         if caps.is_none() {
178             continue;
179         }
180         let caps = caps.unwrap();
181         let loop_device = caps.get(1).unwrap().as_str();
182         let loop_device = format!("/dev/loop{}", loop_device);
183         return Ok(loop_device);
184     }
185     Err(anyhow!("Loop device not found"))
186 }
187 
188 #[cfg(test)]
189 mod tests {
190     use super::*;
191 
192     #[test]
193     fn test_regex_find_loop_device() {
194         const DEVICE_NAME_SHOULD_MATCH: [&str; 3] =
195             ["/dev/loop11", "/dev/loop11p1", "/dev/loop11p1 "];
196         let device_name = "/dev/loop11";
197         let re = Regex::new(LOOP_DEVICE_LOSETUP_A_REGEX).unwrap();
198         for name in DEVICE_NAME_SHOULD_MATCH {
199             assert!(re.find(name).is_some(), "{} should match", name);
200             assert_eq!(
201                 re.find(name).unwrap().as_str(),
202                 device_name,
203                 "{} should match {}",
204                 name,
205                 device_name
206             );
207         }
208     }
209 
210     #[test]
211     fn test_parse_losetup_a_output() {
212         let losetup_a_output = r#"/dev/loop1: []: (/data/bin/disk-image-x86_64.img)
213 /dev/loop29: []: (/var/lib/abc.img)
214 /dev/loop13: []: (/var/lib/snapd/snaps/gtk-common-themes_1535.snap
215 /dev/loop19: []: (/var/lib/snapd/snaps/gnome-42-2204_172.snap)"#;
216         let disk_img_path = "/data/bin/disk-image-x86_64.img";
217         let loop_device_path =
218             __loop_device_path_by_disk_image_path(disk_img_path, losetup_a_output).unwrap();
219         assert_eq!(loop_device_path, "/dev/loop1");
220     }
221 
222     #[test]
223     fn test_parse_lsblk_output_not_match() {
224         let losetup_a_output = r#"/dev/loop1: []: (/data/bin/disk-image-x86_64.img)
225 /dev/loop29: []: (/var/lib/abc.img)
226 /dev/loop13: []: (/var/lib/snapd/snaps/gtk-common-themes_1535.snap
227 /dev/loop19: []: (/var/lib/snapd/snaps/gnome-42-2204_172.snap)"#;
228         let disk_img_path = "/data/bin/disk-image-riscv64.img";
229         let loop_device_path =
230             __loop_device_path_by_disk_image_path(disk_img_path, losetup_a_output);
231         assert!(
232             loop_device_path.is_err(),
233             "should not match any loop device"
234         );
235     }
236 }
237