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;
pub struct FlipperStorage {
cli: SerialCli,
}
impl FlipperStorage {
pub fn new(port: Box<dyn SerialPort>) -> Self {
Self {
cli: SerialCli::new(port),
}
}
pub fn start(&mut self) -> io::Result<()> {
self.cli.start()
}
pub fn port(&self) -> &dyn SerialPort {
self.cli.port()
}
pub fn port_mut(&mut self) -> &mut dyn SerialPort {
self.cli.port_mut()
}
pub fn cli_mut(&mut self) -> &mut SerialCli {
&mut self.cli
}
pub fn list_tree(&mut self, path: &FlipperPath) -> io::Result<()> {
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 {
"[D]" => {
let path = path.clone() + info;
eprintln!("{path}");
self.list_tree(&path)?;
}
"[F]" => {
if let Some((name, size)) = info.rsplit_once(' ') {
let path = path.clone() + name;
eprintln!("{path}, size {size}");
}
}
_ => (),
}
}
}
Ok(())
}
pub fn send_file(&mut self, from: impl AsRef<Path>, to: &FlipperPath) -> io::Result<()> {
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(())
}
pub fn receive_file(&mut self, from: &FlipperPath, to: impl AsRef<Path>) -> 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(())
}
pub fn read_file(&mut self, path: &FlipperPath) -> io::Result<BytesMut> {
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)
}
pub fn exist(&mut self, path: &FlipperPath) -> io::Result<bool> {
let exist = match self.stat(path) {
Err(_err) => false,
Ok(_) => true,
};
Ok(exist)
}
pub fn exist_dir(&mut self, path: &FlipperPath) -> io::Result<bool> {
let exist = match self.stat(path) {
Err(_err) => false,
Ok(stat) => stat.contains("Directory") || stat.contains("Storage"),
};
Ok(exist)
}
pub fn exist_file(&mut self, path: &FlipperPath) -> io::Result<bool> {
let exist = match self.stat(path) {
Err(_err) => false,
Ok(stat) => stat.contains("File, size:"),
};
Ok(exist)
}
pub fn size(&mut self, path: &FlipperPath) -> io::Result<usize> {
let line = self.stat(path)?;
let size = Regex::new(r"File, size: (.+)b")
.unwrap()
.captures(&line)
.and_then(|m| m[1].parse::<usize>().ok())
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to parse size"))?;
Ok(size)
}
fn stat(&mut self, path: &FlipperPath) -> io::Result<String> {
self.cli
.send_and_wait_eol(&format!("storage stat {path}"))?;
let line = self.cli.consume_response()?;
Ok(line)
}
pub fn mkdir(&mut self, path: &FlipperPath) -> io::Result<()> {
self.cli
.send_and_wait_eol(&format!("storage mkdir {path}"))?;
self.cli.consume_response()?;
Ok(())
}
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(())
}
pub fn remove(&mut self, path: &FlipperPath) -> io::Result<()> {
self.cli
.send_and_wait_eol(&format!("storage remove {path}"))?;
self.cli.consume_response()?;
Ok(())
}
pub fn md5sum(&mut self, path: &FlipperPath) -> io::Result<String> {
self.cli.send_and_wait_eol(&format!("storage md5 {path}"))?;
let line = self.cli.consume_response()?;
Ok(line)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FlipperPath(String);
impl FlipperPath {
pub fn new() -> Self {
Self(String::from("/"))
}
pub fn push(&mut self, path: &str) {
let path = path.trim_end_matches('/');
if path.starts_with('/') {
self.0 = String::from(path);
} else {
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<str> for FlipperPath {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl From<String> for FlipperPath {
fn from(mut value: String) -> Self {
if let Some(p) = value.rfind(|c| c != '/') {
value.truncate(p + 1);
}
if !value.starts_with('/') {
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
}
}