unsquashfs_wrapper/
lib.rs1use std::{
2 io::{self, BufReader, Error, ErrorKind, Read},
3 path::Path,
4 process::{ChildStdout, Command, Stdio},
5 str,
6 sync::{
7 atomic::{AtomicBool, Ordering},
8 Arc, RwLock,
9 },
10 thread,
11 time::Duration,
12};
13
14use thiserror::Error;
15
16fn handle(stdout: ChildStdout, mut callback: impl FnMut(i32)) -> io::Result<()> {
17 let mut last_progress = 0;
18 let mut reader = BufReader::new(stdout);
19
20 loop {
21 let mut data = [0; 0x1000];
22 let count = reader.read(&mut data)?;
23
24 if count == 0 {
25 return Ok(());
26 }
27
28 if let Ok(string) = str::from_utf8(&data[..count]) {
29 for line in string.split(['\r', '\n']) {
30 let len = line.len();
31 if line.starts_with('[') && line.ends_with('%') && len >= 4 {
32 if let Ok(progress) = line[len - 4..len - 1].trim().parse::<i32>() {
33 if last_progress != progress {
34 callback(progress);
35 last_progress = progress;
36 }
37 }
38 }
39 }
40 }
41 }
42}
43
44#[derive(Clone)]
45pub struct Unsquashfs {
46 cancel: Arc<AtomicBool>,
47 status: Arc<RwLock<Status>>,
48}
49
50pub enum Status {
51 Pending,
52 Working,
53}
54
55impl Default for Unsquashfs {
56 fn default() -> Self {
57 Self {
58 cancel: Arc::new(AtomicBool::new(false)),
59 status: Arc::new(RwLock::new(Status::Pending)),
60 }
61 }
62}
63
64#[derive(Debug, Error)]
65pub enum UnsquashfsError {
66 #[error("`unsquashfs` binary does not exist.")]
67 BinaryDoesNotExist,
68 #[error(transparent)]
69 IO(#[from] io::Error),
70 #[error("`unsquashfs` is not start.")]
71 Pending,
72 #[error("`unsquashfs` failed: {0}, output: {1}")]
73 Failure(io::Error, String),
74}
75
76impl Unsquashfs {
77 pub fn new() -> Self {
78 Unsquashfs::default()
79 }
80
81 pub fn cancel(&self) -> Result<(), UnsquashfsError> {
82 match *self.status.read().unwrap() {
83 Status::Pending => Err(UnsquashfsError::Pending),
84 Status::Working => {
85 self.cancel.store(true, Ordering::SeqCst);
86 Ok(())
87 }
88 }
89 }
90
91 pub fn extract(
93 &self,
94 archive: impl AsRef<Path>,
95 directory: impl AsRef<Path>,
96 thread: Option<usize>,
97 callback: impl FnMut(i32),
98 ) -> Result<(), UnsquashfsError> {
99 if which::which("unsquashfs").is_err() {
100 return Err(UnsquashfsError::BinaryDoesNotExist);
101 }
102
103 let archive = archive.as_ref().canonicalize()?;
104 let directory = directory.as_ref().canonicalize()?;
105
106 let directory = directory
107 .to_str()
108 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "Invalid directory path"))?
109 .replace('\'', "'\"'\"'");
110
111 let archive = archive
112 .to_str()
113 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "Invalid archive path"))?
114 .replace('\'', "'\"'\"'");
115
116 let mut command = Command::new("unsquashfs");
117
118 if let Some(limit_thread) = thread {
119 command.arg("-p").arg(limit_thread.to_string());
120 }
121
122 command
123 .arg("-f")
124 .arg("-q")
125 .arg("-d")
126 .arg(directory)
127 .arg(archive);
128
129 let mut child = command
130 .env("COLUMNS", "")
131 .env("LINES", "")
132 .env("TERM", "xterm-256color")
133 .stdout(Stdio::piped())
134 .stderr(Stdio::piped())
135 .spawn()?;
136
137 *self.status.write().unwrap() = Status::Working;
138
139 let cc = self.cancel.clone();
140 let status_clone = self.status.clone();
141
142 let stdout = child
143 .stdout
144 .take()
145 .ok_or_else(|| io::Error::new(ErrorKind::BrokenPipe, "Failed to get stdout"))?;
146
147 let stderr = child
148 .stderr
149 .take()
150 .ok_or_else(|| io::Error::new(ErrorKind::BrokenPipe, "Failed to get stderr"))?;
151
152 let process_control = thread::spawn(move || -> io::Result<()> {
153 loop {
154 let wait = child.try_wait()?;
155
156 if cc.load(Ordering::SeqCst) {
157 child.kill()?;
158 cc.store(false, Ordering::SeqCst);
159 *status_clone.write().unwrap() = Status::Pending;
160 return Ok(());
161 }
162
163 let Some(wait) = wait else {
164 thread::sleep(Duration::from_millis(10));
165 continue;
166 };
167
168 *status_clone.write().unwrap() = Status::Pending;
169
170 if !wait.success() {
171 return Err(Error::new(
172 ErrorKind::Other,
173 format!(
174 "archive extraction failed with status: {}",
175 wait.code().unwrap_or(1),
176 ),
177 ));
178 } else {
179 return Ok(());
180 }
181 }
182 });
183
184 handle(stdout, callback)?;
185
186 let mut stderr = BufReader::new(stderr);
187 let mut buf = String::new();
188 stderr.read_to_string(&mut buf).ok();
189
190 process_control
191 .join()
192 .unwrap()
193 .map_err(|e| UnsquashfsError::Failure(e, buf))?;
194
195 Ok(())
196 }
197}
198
199#[cfg(test)]
200pub mod test {
201 use std::{env::temp_dir, fs, thread, time::Duration};
202
203 use crate::Unsquashfs;
204
205 #[test]
206 fn test_extract() {
207 let unsquashfs = Unsquashfs::default();
208 let unsquashfs_clone = unsquashfs.clone();
209
210 let t = thread::spawn(move || {
211 let output = temp_dir().join("unsqfs-wrap-test-extract");
212 fs::create_dir_all(&output).unwrap();
213 unsquashfs
214 .extract(
215 "testdata/test_extract.squashfs",
216 &output,
217 None,
218 Box::new(move |c| {
219 dbg!(c);
220 }),
221 )
222 .unwrap();
223 fs::remove_dir_all(output).unwrap();
224 });
225
226 thread::sleep(Duration::from_millis(10));
227 unsquashfs_clone.cancel().unwrap();
228
229 t.join().unwrap();
230 }
231}