//! Storage interface. use std::fmt::Display; use std::io::{Read, Write}; use std::ops::Add; use std::path::Path; use std::{fs, io}; use bytes::BytesMut; use regex::Regex; use serialport::SerialPort; use crate::serial::{SerialCli, CLI_EOL}; const BUF_SIZE: usize = 1024; /// Interface to Flipper device storage. pub struct FlipperStorage { cli: SerialCli, } impl FlipperStorage { /// Create new [`FlipperStorage`] connected to a [`SerialPort`]. pub fn new(port: Box) -> Self { Self { cli: SerialCli::new(port), } } /// Start serial interface. pub fn start(&mut self) -> io::Result<()> { self.cli.start() } /// Get reference to underlying [`SerialPort`]. pub fn port(&self) -> &dyn SerialPort { self.cli.port() } /// Get mutable reference to underlying [`SerialPort`]. pub fn port_mut(&mut self) -> &mut dyn SerialPort { self.cli.port_mut() } /// Get mutable reference to underlying [`SerialCli`]. pub fn cli_mut(&mut self) -> &mut SerialCli { &mut self.cli } /// List files and directories on the device. pub fn list_tree(&mut self, path: &FlipperPath) -> io::Result<()> { // Note: The `storage list` command expects that paths do not end with a slash. self.cli .send_and_wait_eol(&format!("storage list {}", path))?; let data = self.cli.read_until_prompt()?; for line in CLI_EOL.split(&data).map(String::from_utf8_lossy) { let line = line.trim(); if line.is_empty() { continue; } if let Some(error) = SerialCli::get_error(line) { eprintln!("ERROR: {error}"); continue; } if line == "Empty" { continue; } if let Some((typ, info)) = line.split_once(' ') { match typ { // Directory "[D]" => { let path = path.clone() + info; eprintln!("{path}"); self.list_tree(&path)?; } // File "[F]" => { if let Some((name, size)) = info.rsplit_once(' ') { let path = path.clone() + name; eprintln!("{path}, size {size}"); } } // We got something unexpected, ignore it _ => (), } } } Ok(()) } /// Send local file to the device. pub fn send_file(&mut self, from: impl AsRef, to: &FlipperPath) -> io::Result<()> { // Try to create directory on Flipper if let Some(dir) = to.0.rsplit_once('/') { self.mkdir(&FlipperPath::from(dir.0)).ok(); } self.remove(to).ok(); let mut file = fs::File::open(from.as_ref())?; let mut buf = [0u8; BUF_SIZE]; loop { let n = file.read(&mut buf)?; if n == 0 { break; } self.cli .send_and_wait_eol(&format!("storage write_chunk \"{to}\" {n}"))?; let line = self.cli.read_until_eol()?; let line = String::from_utf8_lossy(&line); if let Some(error) = SerialCli::get_error(&line) { self.cli.read_until_prompt()?; return Err(io::Error::new(io::ErrorKind::Other, error)); } self.port_mut().write_all(&buf[..n])?; self.cli.read_until_prompt()?; } Ok(()) } /// Receive remote file from the device. pub fn receive_file(&mut self, from: &FlipperPath, to: impl AsRef) -> io::Result<()> { let mut file = fs::File::options() .create(true) .truncate(true) .write(true) .open(to.as_ref())?; let data = self.read_file(from)?; file.write_all(&data)?; Ok(()) } /// Read file data from the device. pub fn read_file(&mut self, path: &FlipperPath) -> io::Result { self.cli .send_and_wait_eol(&format!("storage read_chunks \"{path}\" {}", BUF_SIZE))?; let line = self.cli.read_until_eol()?; let line = String::from_utf8_lossy(&line); if let Some(error) = SerialCli::get_error(&line) { self.cli.read_until_prompt()?; return Err(io::Error::new(io::ErrorKind::Other, error)); } let (_, size) = line .split_once(": ") .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to read chunk size"))?; let size: usize = size .parse() .map_err(|_| io::Error::new(io::ErrorKind::Other, "failed to parse chunk size"))?; let mut data = BytesMut::with_capacity(BUF_SIZE); let mut buf = [0u8; BUF_SIZE]; while data.len() < size { self.cli.read_until_ready()?; self.cli.send_line("y")?; let n = (size - data.len()).min(BUF_SIZE); self.port_mut().read_exact(&mut buf[..n])?; data.extend_from_slice(&buf[..n]); } Ok(data) } /// Does the file or directory exist on the device? pub fn exist(&mut self, path: &FlipperPath) -> io::Result { let exist = match self.stat(path) { Err(_err) => false, Ok(_) => true, }; Ok(exist) } /// Does the directory exist on the device? pub fn exist_dir(&mut self, path: &FlipperPath) -> io::Result { let exist = match self.stat(path) { Err(_err) => false, Ok(stat) => stat.contains("Directory") || stat.contains("Storage"), }; Ok(exist) } /// Does the file exist on the device? pub fn exist_file(&mut self, path: &FlipperPath) -> io::Result { let exist = match self.stat(path) { Err(_err) => false, Ok(stat) => stat.contains("File, size:"), }; Ok(exist) } /// File size in bytes pub fn size(&mut self, path: &FlipperPath) -> io::Result { let line = self.stat(path)?; let size = Regex::new(r"File, size: (.+)b") .unwrap() .captures(&line) .and_then(|m| m[1].parse::().ok()) .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to parse size"))?; Ok(size) } /// Stat a file or directory. fn stat(&mut self, path: &FlipperPath) -> io::Result { self.cli .send_and_wait_eol(&format!("storage stat {path}"))?; let line = self.cli.consume_response()?; Ok(line) } /// Make directory on the device. pub fn mkdir(&mut self, path: &FlipperPath) -> io::Result<()> { self.cli .send_and_wait_eol(&format!("storage mkdir {path}"))?; self.cli.consume_response()?; Ok(()) } /// Format external storage. pub fn format_ext(&mut self) -> io::Result<()> { self.cli.send_and_wait_eol("storage format /ext")?; self.cli.send_and_wait_eol("y")?; self.cli.consume_response()?; Ok(()) } /// Remove file or directory. pub fn remove(&mut self, path: &FlipperPath) -> io::Result<()> { self.cli .send_and_wait_eol(&format!("storage remove {path}"))?; self.cli.consume_response()?; Ok(()) } /// Calculate MD5 hash of file. pub fn md5sum(&mut self, path: &FlipperPath) -> io::Result { self.cli.send_and_wait_eol(&format!("storage md5 {path}"))?; let line = self.cli.consume_response()?; Ok(line) } } /// A path on the Flipper device. /// /// [`FlipperPath`] maintains certain invariants: /// - Paths are valid UTF-8 /// - Paths are always absolute (start with `/`) /// - Paths do not end in a `/` #[derive(Clone, Debug, PartialEq, Eq)] pub struct FlipperPath(String); impl FlipperPath { /// Create a new [`FlipperPath`]. pub fn new() -> Self { Self(String::from("/")) } /// Push a path fragment to this path pub fn push(&mut self, path: &str) { let path = path.trim_end_matches('/'); if path.starts_with('/') { // Absolute path self.0 = String::from(path); } else { // Relative path if !self.0.ends_with('/') { self.0 += "/"; } self.0 += path; } } } impl Default for FlipperPath { fn default() -> Self { Self::new() } } impl Display for FlipperPath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } impl AsRef for FlipperPath { fn as_ref(&self) -> &str { self.0.as_str() } } impl From for FlipperPath { fn from(mut value: String) -> Self { if let Some(p) = value.rfind(|c| c != '/') { // Drop any trailing `/` value.truncate(p + 1); } if !value.starts_with('/') { // Make path absolute let mut path = Self::new(); path.0.extend([value]); path } else { Self(value) } } } impl From<&str> for FlipperPath { fn from(value: &str) -> Self { FlipperPath::from(value.to_string()) } } impl Add<&str> for FlipperPath { type Output = Self; fn add(mut self, rhs: &str) -> Self::Output { self.push(rhs); self } }