flipperzero_tools/
storage.rs1use 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
17pub struct FlipperStorage {
19 cli: SerialCli,
20}
21
22impl FlipperStorage {
23 pub fn new(port: Box<dyn SerialPort>) -> Self {
25 Self {
26 cli: SerialCli::new(port),
27 }
28 }
29
30 pub fn start(&mut self) -> io::Result<()> {
32 self.cli.start()
33 }
34
35 pub fn port(&self) -> &dyn SerialPort {
37 self.cli.port()
38 }
39
40 pub fn port_mut(&mut self) -> &mut dyn SerialPort {
42 self.cli.port_mut()
43 }
44
45 pub fn cli_mut(&mut self) -> &mut SerialCli {
47 &mut self.cli
48 }
49
50 pub fn list_tree(&mut self, path: &FlipperPath) -> io::Result<()> {
52 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 "[D]" => {
76 let path = path.clone() + info;
77
78 eprintln!("{path}");
79 self.list_tree(&path)?;
80 }
81 "[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 _ => (),
91 }
92 }
93 }
94
95 Ok(())
96 }
97
98 pub fn send_file(&mut self, from: impl AsRef<Path>, to: &FlipperPath) -> io::Result<()> {
100 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct FlipperPath(String);
278
279impl FlipperPath {
280 pub fn new() -> Self {
282 Self(String::from("/"))
283 }
284
285 pub fn push(&mut self, path: &str) {
287 let path = path.trim_end_matches('/');
288 if path.starts_with('/') {
289 self.0 = String::from(path);
291 } else {
292 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 value.truncate(p + 1);
324 }
325
326 if !value.starts_with('/') {
327 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}