1use serde_json::{Value, json};
3use std::io::Read;
4#[cfg(any(target_os = "macos", windows))]
5use std::process::{Command, Stdio};
6#[cfg(any(target_os = "macos", windows))]
7use std::time::{Duration, Instant};
8#[cfg(any(target_os = "macos", test))]
9#[path = "volumes_ipod.rs"]
10mod ipod;
11#[cfg(target_os = "macos")]
12#[path = "volumes_macos.rs"]
13mod macos;
14#[cfg(any(target_os = "macos", test))]
15#[path = "volumes_plist.rs"]
16mod plist;
17#[derive(Debug, PartialEq, Eq)]
19pub struct Volume {
20 pub name: String,
22 pub path: String,
24 pub format: String,
26 pub kind: String,
28 pub model: Option<&'static str>,
30 pub icon: &'static str,
32 pub ipod_supported: Option<bool>,
34 pub ejectable: bool,
36 #[cfg(target_os = "macos")]
37 device: Option<String>,
38}
39impl Volume {
40 pub fn json(&self) -> Value {
42 json!({"name":self.name,"path":self.path,"format":self.format,"kind":self.kind,"model":self.model,"icon":self.icon,"ipodSupported":self.ipod_supported,"ejectable":self.ejectable})
43 }
44 pub fn eject(&self, confirm: impl Fn() -> Result<(), String>) -> Result<(), String> {
47 #[cfg(target_os = "macos")]
48 {
49 macos::eject(self, confirm)
50 }
51 #[cfg(not(target_os = "macos"))]
52 {
53 let _ = confirm;
54 Err("Éjectez ce lecteur depuis votre système d’exploitation.".into())
55 }
56 }
57}
58fn volume(path: &str, format: &str, kind: &str) -> Volume {
59 Volume {
60 name: std::path::Path::new(path)
61 .file_name()
62 .and_then(|name| name.to_str())
63 .unwrap_or(path)
64 .to_owned(),
65 path: path.into(),
66 format: format.into(),
67 kind: kind.into(),
68 model: None,
69 icon: "drive",
70 ipod_supported: None,
71 ejectable: false,
72 #[cfg(target_os = "macos")]
73 device: None,
74 }
75}
76#[cfg(any(target_os = "macos", target_os = "linux", test))]
77fn unescape(path: &str) -> String {
78 path.replace("\\040", " ")
79 .replace("\\011", "\t")
80 .replace("\\012", "\n")
81 .replace("\\134", "\\")
82}
83#[cfg(any(target_os = "macos", target_os = "linux", test))]
84fn parse_mounts(text: &str, mac: bool) -> Vec<Volume> {
85 let mut rows = Vec::new();
86 for line in text.lines() {
87 let parsed = if mac {
88 line.split_once(" on ")
89 .and_then(|(_, tail)| tail.rsplit_once(" ("))
90 .map(|(path, flags)| {
91 (
92 path,
93 flags.split(',').next().unwrap_or("").trim_end_matches(')'),
94 )
95 })
96 } else {
97 line.split_once(" - ").and_then(|(fields, tail)| {
98 Some((
99 fields.split_whitespace().nth(4)?,
100 tail.split_whitespace().next()?,
101 ))
102 })
103 };
104 let Some((path, format)) = parsed else {
105 continue;
106 };
107 let path = unescape(path);
108 let network = matches!(
109 format,
110 "smbfs" | "cifs" | "nfs" | "nfs4" | "sshfs" | "fuse.sshfs"
111 );
112 let media = if mac {
113 path.starts_with("/Volumes/")
114 } else {
115 ["/media/", "/run/media/", "/mnt/"]
116 .iter()
117 .any(|root| path.starts_with(root))
118 };
119 if path != "/"
120 && path.starts_with('/')
121 && (media || network)
122 && !rows.iter().any(|row: &Volume| row.path == path)
123 {
124 rows.push(volume(
125 &path,
126 format,
127 if network { "network" } else { "volume" },
128 ));
129 }
130 }
131 rows
132}
133#[cfg(any(windows, test))]
134fn parse_windows(bytes: &[u8]) -> Result<Vec<Volume>, String> {
135 let rows: Vec<Value> =
136 serde_json::from_slice(bytes).map_err(|_| "Détection des lecteurs illisible.")?;
137 rows.into_iter()
138 .map(|row| {
139 let path = row["path"].as_str().ok_or("Lecteur invalide.")?;
140 let format = row["format"].as_str().ok_or("Lecteur invalide.")?;
141 let kind = row["kind"].as_str().ok_or("Lecteur invalide.")?;
142 if path.len() != 3
143 || !path.as_bytes()[0].is_ascii_alphabetic()
144 || &path[1..] != ":\\"
145 || !matches!(kind, "removable" | "network" | "volume")
146 {
147 return Err("Lecteur invalide.".into());
148 }
149 Ok(volume(path, format, kind))
150 })
151 .collect()
152}
153#[cfg(any(target_os = "macos", windows))]
154fn output(program: &str, args: &[&str], timeout: Duration) -> Result<Vec<u8>, String> {
155 let mut child = Command::new(program)
156 .args(args)
157 .stdout(Stdio::piped())
158 .stderr(Stdio::null())
159 .spawn()
160 .map_err(|_| "Détection des lecteurs indisponible.")?;
161 let stdout = child
162 .stdout
163 .take()
164 .ok_or("Détection des lecteurs indisponible.")?;
165 let reader = std::thread::spawn(move || {
166 let mut bytes = Vec::new();
167 stdout.take(262145).read_to_end(&mut bytes).map(|_| bytes)
168 });
169 let deadline = Instant::now() + timeout;
170 let status = loop {
171 if let Some(status) = child
172 .try_wait()
173 .map_err(|_| "Détection des lecteurs indisponible.")?
174 {
175 break status;
176 }
177 if Instant::now() >= deadline {
178 let _ = child.kill();
179 let _ = child.wait();
180 return Err("La détection des lecteurs a expiré.".into());
181 }
182 std::thread::sleep(Duration::from_millis(10));
183 };
184 let bytes = reader
185 .join()
186 .map_err(|_| "Détection des lecteurs interrompue.")?
187 .map_err(|_| "Détection des lecteurs illisible.")?;
188 if !status.success() || bytes.len() > 262144 {
189 return Err("Détection des lecteurs illisible.".into());
190 }
191 Ok(bytes)
192}
193pub fn discover() -> Result<Vec<Volume>, String> {
195 #[cfg(target_os = "macos")]
196 {
197 let deadline = Instant::now() + Duration::from_secs(3);
198 let bytes = output("/sbin/mount", &[], Duration::from_secs(3))?;
199 let candidates = parse_mounts(
200 std::str::from_utf8(&bytes).map_err(|_| "Détection des lecteurs illisible.")?,
201 true,
202 );
203 let usb = macos::usb_tree();
204 let mut rows = Vec::new();
205 for mut row in candidates {
206 if row.kind == "network" {
207 rows.push(row);
208 continue;
209 }
210 let remaining = deadline.saturating_duration_since(Instant::now());
211 if remaining.is_zero() {
212 return Err("La détection des lecteurs a expiré.".into());
213 }
214 if macos::decorate(&mut row, usb.as_ref(), remaining)? {
215 rows.push(row);
216 }
217 }
218 Ok(rows)
219 }
220 #[cfg(target_os = "linux")]
221 {
222 let file = std::fs::File::open("/proc/self/mountinfo")
223 .map_err(|_| "Détection des lecteurs indisponible.")?;
224 let mut text = String::new();
225 file.take(262145)
226 .read_to_string(&mut text)
227 .map_err(|_| "Détection des lecteurs illisible.")?;
228 if text.len() > 262144 {
229 return Err("Détection des lecteurs illisible.".into());
230 }
231 Ok(parse_mounts(&text, false))
232 }
233 #[cfg(windows)]
234 {
235 let bytes = output(
236 "powershell.exe",
237 &[
238 "-NoProfile",
239 "-NonInteractive",
240 "-Command",
241 "ConvertTo-Json -Compress -InputObject @(Get-CimInstance Win32_LogicalDisk | Where-Object { $_.DriveType -in 2,4 -or ($_.DriveType -eq 3 -and $_.DeviceID -ne $env:SystemDrive) } | ForEach-Object { @{ path=($_.DeviceID+'\\'); kind=$(if($_.DriveType -eq 4){'network'}elseif($_.DriveType -eq 2){'removable'}else{'volume'}); format=[string]$_.FileSystem } })",
242 ],
243 Duration::from_secs(3),
244 )?;
245 parse_windows(&bytes)
246 }
247 #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
248 {
249 Err("Détection des lecteurs indisponible sur cette plateforme.".into())
250 }
251}
252#[cfg(test)]
253#[path = "volumes_tests.rs"]
254mod tests;