flipperzero_tools/
storage.rs

1//! Storage interface.
2
3use std::fmt::Display;
4use std::io::{Read, Write};
5use std::ops::Add;
6use std::path::Path;
7use std::{fs, io};
8
9use bytes::BytesMut;
10use regex::Regex;
11use serialport::SerialPort;
12
13use crate::serial::{SerialCli, CLI_EOL};
14
15const BUF_SIZE: usize = 1024;
16
17/// Interface to Flipper device storage.
18pub struct FlipperStorage {
19    cli: SerialCli,
20}
21
22impl FlipperStorage {
23    /// Create new [`FlipperStorage`] connected to a [`SerialPort`].
24    pub fn new(port: Box<dyn SerialPort>) -> Self {
25        Self {
26            cli: SerialCli::new(port),
27        }
28    }
29
30    /// Start serial interface.
31    pub fn start(&mut self) -> io::Result<()> {
32        self.cli.start()
33    }
34
35    /// Get reference to underlying [`SerialPort`].
36    pub fn port(&self) -> &dyn SerialPort {
37        self.cli.port()
38    }
39
40    /// Get mutable reference to underlying [`SerialPort`].
41    pub fn port_mut(&mut self) -> &mut dyn SerialPort {
42        self.cli.port_mut()
43    }
44
45    /// Get mutable reference to underlying [`SerialCli`].
46    pub fn cli_mut(&mut self) -> &mut SerialCli {
47        &mut self.cli
48    }
49
50    /// List files and directories on the device.
51    pub fn list_tree(&mut self, path: &FlipperPath) -> io::Result<()> {
52        // Note: The `storage list` command expects that paths do not end with a slash.
53        self.cli
54            .send_and_wait_eol(&format!("storage list {}", path))?;
55
56        let data = self.cli.read_until_prompt()?;
57        for line in CLI_EOL.split(&data).map(String::from_utf8_lossy) {
58            let line = line.trim();
59            if line.is_empty() {
60                continue;
61            }
62
63            if let Some(error) = SerialCli::get_error(line) {
64                eprintln!("ERROR: {error}");
65                continue;
66            }
67
68            if line == "Empty" {
69                continue;
70            }
71
72            if let Some((typ, info)) = line.split_once(' ') {
73                match typ {
74                    // Directory
75                    "[D]" => {
76                        let path = path.clone() + info;
77
78                        eprintln!("{path}");
79                        self.list_tree(&path)?;
80                    }
81                    // File
82                    "[F]" => {
83                        if let Some((name, size)) = info.rsplit_once(' ') {
84                            let path = path.clone() + name;
85
86                            eprintln!("{path}, size {size}");
87                        }
88                    }
89                    // We got something unexpected, ignore it
90                    _ => (),
91                }
92            }
93        }
94
95        Ok(())
96    }
97
98    /// Send local file to the device.
99    pub fn send_file(&mut self, from: impl AsRef<Path>, to: &FlipperPath) -> io::Result<()> {
100        // Try to create directory on Flipper
101        if let Some(dir) = to.0.rsplit_once('/') {
102            self.mkdir(&FlipperPath::from(dir.0)).ok();
103        }
104        self.remove(to).ok();
105
106        let mut file = fs::File::open(from.as_ref())?;
107
108        let mut buf = [0u8; BUF_SIZE];
109        loop {
110            let n = file.read(&mut buf)?;
111            if n == 0 {
112                break;
113            }
114
115            self.cli
116                .send_and_wait_eol(&format!("storage write_chunk \"{to}\" {n}"))?;
117            let line = self.cli.read_until_eol()?;
118            let line = String::from_utf8_lossy(&line);
119
120            if let Some(error) = SerialCli::get_error(&line) {
121                self.cli.read_until_prompt()?;
122
123                return Err(io::Error::new(io::ErrorKind::Other, error));
124            }
125
126            self.port_mut().write_all(&buf[..n])?;
127            self.cli.read_until_prompt()?;
128        }
129
130        Ok(())
131    }
132
133    /// Receive remote file from the device.
134    pub fn receive_file(&mut self, from: &FlipperPath, to: impl AsRef<Path>) -> io::Result<()> {
135        let mut file = fs::File::options()
136            .create(true)
137            .truncate(true)
138            .write(true)
139            .open(to.as_ref())?;
140
141        let data = self.read_file(from)?;
142        file.write_all(&data)?;
143
144        Ok(())
145    }
146
147    /// Read file data from the device.
148    pub fn read_file(&mut self, path: &FlipperPath) -> io::Result<BytesMut> {
149        self.cli
150            .send_and_wait_eol(&format!("storage read_chunks \"{path}\" {}", BUF_SIZE))?;
151        let line = self.cli.read_until_eol()?;
152        let line = String::from_utf8_lossy(&line);
153
154        if let Some(error) = SerialCli::get_error(&line) {
155            self.cli.read_until_prompt()?;
156
157            return Err(io::Error::new(io::ErrorKind::Other, error));
158        }
159
160        let (_, size) = line
161            .split_once(": ")
162            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to read chunk size"))?;
163        let size: usize = size
164            .parse()
165            .map_err(|_| io::Error::new(io::ErrorKind::Other, "failed to parse chunk size"))?;
166
167        let mut data = BytesMut::with_capacity(BUF_SIZE);
168
169        let mut buf = [0u8; BUF_SIZE];
170        while data.len() < size {
171            self.cli.read_until_ready()?;
172            self.cli.send_line("y")?;
173
174            let n = (size - data.len()).min(BUF_SIZE);
175            self.port_mut().read_exact(&mut buf[..n])?;
176            data.extend_from_slice(&buf[..n]);
177        }
178
179        Ok(data)
180    }
181
182    /// Does the file or directory exist on the device?
183    pub fn exist(&mut self, path: &FlipperPath) -> io::Result<bool> {
184        let exist = match self.stat(path) {
185            Err(_err) => false,
186            Ok(_) => true,
187        };
188
189        Ok(exist)
190    }
191
192    /// Does the directory exist on the device?
193    pub fn exist_dir(&mut self, path: &FlipperPath) -> io::Result<bool> {
194        let exist = match self.stat(path) {
195            Err(_err) => false,
196            Ok(stat) => stat.contains("Directory") || stat.contains("Storage"),
197        };
198
199        Ok(exist)
200    }
201
202    /// Does the file exist on the device?
203    pub fn exist_file(&mut self, path: &FlipperPath) -> io::Result<bool> {
204        let exist = match self.stat(path) {
205            Err(_err) => false,
206            Ok(stat) => stat.contains("File, size:"),
207        };
208
209        Ok(exist)
210    }
211
212    /// File size in bytes
213    pub fn size(&mut self, path: &FlipperPath) -> io::Result<usize> {
214        let line = self.stat(path)?;
215
216        let size = Regex::new(r"File, size: (.+)b")
217            .unwrap()
218            .captures(&line)
219            .and_then(|m| m[1].parse::<usize>().ok())
220            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to parse size"))?;
221
222        Ok(size)
223    }
224
225    /// Stat a file or directory.
226    fn stat(&mut self, path: &FlipperPath) -> io::Result<String> {
227        self.cli
228            .send_and_wait_eol(&format!("storage stat {path}"))?;
229        let line = self.cli.consume_response()?;
230
231        Ok(line)
232    }
233
234    /// Make directory on the device.
235    pub fn mkdir(&mut self, path: &FlipperPath) -> io::Result<()> {
236        self.cli
237            .send_and_wait_eol(&format!("storage mkdir {path}"))?;
238        self.cli.consume_response()?;
239
240        Ok(())
241    }
242
243    /// Format external storage.
244    pub fn format_ext(&mut self) -> io::Result<()> {
245        self.cli.send_and_wait_eol("storage format /ext")?;
246        self.cli.send_and_wait_eol("y")?;
247        self.cli.consume_response()?;
248
249        Ok(())
250    }
251
252    /// Remove file or directory.
253    pub fn remove(&mut self, path: &FlipperPath) -> io::Result<()> {
254        self.cli
255            .send_and_wait_eol(&format!("storage remove {path}"))?;
256        self.cli.consume_response()?;
257
258        Ok(())
259    }
260
261    /// Calculate MD5 hash of file.
262    pub fn md5sum(&mut self, path: &FlipperPath) -> io::Result<String> {
263        self.cli.send_and_wait_eol(&format!("storage md5 {path}"))?;
264        let line = self.cli.consume_response()?;
265
266        Ok(line)
267    }
268}
269
270/// A path on the Flipper device.
271///
272/// [`FlipperPath`] maintains certain invariants:
273/// - Paths are valid UTF-8
274/// - Paths are always absolute (start with `/`)
275/// - Paths do not end in a `/`
276#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct FlipperPath(String);
278
279impl FlipperPath {
280    /// Create a new [`FlipperPath`].
281    pub fn new() -> Self {
282        Self(String::from("/"))
283    }
284
285    /// Push a path fragment to this path
286    pub fn push(&mut self, path: &str) {
287        let path = path.trim_end_matches('/');
288        if path.starts_with('/') {
289            // Absolute path
290            self.0 = String::from(path);
291        } else {
292            // Relative path
293            if !self.0.ends_with('/') {
294                self.0 += "/";
295            }
296            self.0 += path;
297        }
298    }
299}
300
301impl Default for FlipperPath {
302    fn default() -> Self {
303        Self::new()
304    }
305}
306
307impl Display for FlipperPath {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        f.write_str(&self.0)
310    }
311}
312
313impl AsRef<str> for FlipperPath {
314    fn as_ref(&self) -> &str {
315        self.0.as_str()
316    }
317}
318
319impl From<String> for FlipperPath {
320    fn from(mut value: String) -> Self {
321        if let Some(p) = value.rfind(|c| c != '/') {
322            // Drop any trailing `/`
323            value.truncate(p + 1);
324        }
325
326        if !value.starts_with('/') {
327            // Make path absolute
328            let mut path = Self::new();
329            path.0.extend([value]);
330
331            path
332        } else {
333            Self(value)
334        }
335    }
336}
337
338impl From<&str> for FlipperPath {
339    fn from(value: &str) -> Self {
340        FlipperPath::from(value.to_string())
341    }
342}
343
344impl Add<&str> for FlipperPath {
345    type Output = Self;
346
347    fn add(mut self, rhs: &str) -> Self::Output {
348        self.push(rhs);
349
350        self
351    }
352}