use std::{fmt::Display, io::BufRead, str::FromStr, sync::LazyLock};
use chrono::{DateTime, Utc};
use regex::{Captures, Regex};
use crate::RustADBError;
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct ADBStatMapping {
pub id: u32,
pub name: String,
}
#[derive(Debug)]
pub struct ADBStatExtendedResponse {
pub path: String,
pub size: u64,
pub blocks: u64,
pub io_blocks: u64,
pub inode: u64,
pub links: u64,
pub perms: u64,
pub user: ADBStatMapping,
pub group: ADBStatMapping,
pub atime: u32,
pub mtime: u32,
pub ctime: u32,
}
impl Display for ADBStatExtendedResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "File: {}", self.path)?;
writeln!(
f,
"Size: {}\tBlocks: {}\t IO blocks: {}",
self.size, self.blocks, self.io_blocks
)?;
writeln!(f, "Inode: {}\tLinks: {}", self.inode, self.links)?;
writeln!(
f,
"Access: ({})\tUid: ({}/{})\tGid: ({}/{})",
self.perms, self.user.id, self.user.name, self.group.id, self.group.name
)?;
writeln!(
f,
"Access: {}",
DateTime::<Utc>::from_timestamp(i64::from(self.atime), 0)
.unwrap_or(DateTime::UNIX_EPOCH),
)?;
writeln!(
f,
"Modify: {}",
DateTime::<Utc>::from_timestamp(i64::from(self.mtime), 0)
.unwrap_or(DateTime::UNIX_EPOCH),
)?;
writeln!(
f,
"Change: {}",
DateTime::<Utc>::from_timestamp(i64::from(self.ctime), 0)
.unwrap_or(DateTime::UNIX_EPOCH)
)?;
Ok(())
}
}
static SECOND_LINE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
"^\\s+Size: (?P<size>\\d+)\\s+Blocks: (?P<blocks>\\d+)\\s+IO Blocks: (?P<io_blocks>\\d+).*$",
)
.expect("wrong syntax for second line regex")
});
static THIRD_LINE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("^.*Inode: (?P<inode>\\d+)\\s+Links: (?P<links>\\d+).*$")
.expect("wrong syntax for third regex")
});
static FOURTH_LINE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("^Access: \\((?P<perms>\\d+).*\\)\\s+Uid: \\(\\s(?P<uid>\\d+)/\\s+(?P<uid_name>.*)\\)\\s+Gid: \\(\\s(?P<gid>\\d+)/\\s+(?P<gid_name>.*)\\)$")
.expect("wrong syntax for fourth regex")
});
impl ADBStatExtendedResponse {
pub(crate) fn try_from(value: &[u8]) -> crate::Result<Option<Self>> {
fn extract_from_regex_group<T: FromStr>(
groups: &Captures,
name: &str,
) -> Result<T, RustADBError> {
groups
.name(name)
.ok_or_else(|| RustADBError::StatResponseError(format!("no group named {name}")))?
.as_str()
.parse::<T>()
.map_err(|_| RustADBError::StatResponseError("cannot parse value".into()))
}
fn parse_date(date: &str) -> crate::Result<u32> {
let date: DateTime<Utc> = date.trim().parse()?;
Ok(u32::try_from(date.timestamp())?)
}
let mut iter_lines = value.lines();
let first_line = iter_lines
.next()
.ok_or_else(|| RustADBError::StatResponseError("no first line".into()))??;
let path = match first_line.strip_prefix(" File: ") {
Some(path) => path.trim(),
None if first_line.ends_with("No such file or directory") => {
return Ok(None);
}
None => return Err(RustADBError::StatResponseError("invalid path line".into())),
};
let second_line = iter_lines
.next()
.ok_or_else(|| RustADBError::StatResponseError("no second line".into()))??;
let second_line_groups = SECOND_LINE_REGEX.captures(&second_line).ok_or_else(|| {
RustADBError::StatResponseError("cannot get capture groups for second line".into())
})?;
let (size, blocks, io_blocks) = (
extract_from_regex_group(&second_line_groups, "size")?,
extract_from_regex_group(&second_line_groups, "blocks")?,
extract_from_regex_group(&second_line_groups, "io_blocks")?,
);
let third_line = iter_lines.next().ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: no third line".into())
})??;
let third_line_groups = THIRD_LINE_REGEX.captures(&third_line).ok_or_else(|| {
RustADBError::StatResponseError("cannot get capture groups for third line".into())
})?;
let (inode, links) = (
extract_from_regex_group(&third_line_groups, "inode")?,
extract_from_regex_group(&third_line_groups, "links")?,
);
let fourth_line = iter_lines.next().ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: no fourth line".into())
})??;
let fourth_line_groups = FOURTH_LINE_REGEX.captures(&fourth_line).ok_or_else(|| {
RustADBError::StatResponseError("cannot get capture groups for fourth line".into())
})?;
let (perms, uid, uid_name, gid, gid_name) = (
extract_from_regex_group(&fourth_line_groups, "perms")?,
extract_from_regex_group(&fourth_line_groups, "uid")?,
extract_from_regex_group(&fourth_line_groups, "uid_name")?,
extract_from_regex_group(&fourth_line_groups, "gid")?,
extract_from_regex_group(&fourth_line_groups, "gid_name")?,
);
let fifth_line = iter_lines.next().ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: no fifth line".into())
})??;
let atime = parse_date(fifth_line.strip_prefix("Access: ").ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: invalid atime line".into())
})?)?;
let sixth_line = iter_lines.next().ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: no sixth line".into())
})??;
let mtime = parse_date(sixth_line.strip_prefix("Modify: ").ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: invalid mtime line".into())
})?)?;
let seventh_line = iter_lines.next().ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: no seventh line".into())
})??;
let ctime = parse_date(seventh_line.strip_prefix("Change: ").ok_or_else(|| {
RustADBError::UnknownResponseType("stat response: invalid ctime line".into())
})?)?;
Ok(Some(Self {
path: path.to_string(),
size,
blocks,
io_blocks,
inode,
links,
perms,
user: ADBStatMapping {
id: uid,
name: uid_name,
},
group: ADBStatMapping {
id: gid,
name: gid_name,
},
atime,
mtime,
ctime,
}))
}
}
#[cfg(test)]
mod tests {
use crate::{ADBStatExtendedResponse, models::adb_stat_extended_response::ADBStatMapping};
#[test]
fn parse_stat_extended_response() {
let response = r" File: /data/local/tmp/bigfile
Size: 1048576 Blocks: 2048 IO Blocks: 512 regular file
Device: fe3ch/65084d Inode: 45880 Links: 1 Device type: 0,0
Access: (0777/-rwxrwxrwx) Uid: ( 2000/ shell) Gid: ( 2000/ shell)
Access: 1970-01-01 01:00:00.000000000 +0100
Modify: 1970-01-01 01:00:00.000000000 +0100
Change: 2024-11-28 16:27:23.276724566 +0100
";
let resp = ADBStatExtendedResponse::try_from(response.as_bytes())
.expect("cannot parse stat extended response")
.expect("no such file or directory");
assert_eq!(resp.path, "/data/local/tmp/bigfile");
assert_eq!(resp.size, 1_048_576);
assert_eq!(resp.blocks, 2048);
assert_eq!(resp.io_blocks, 512);
assert_eq!(resp.links, 1);
assert_eq!(
resp.user,
ADBStatMapping {
id: 2000,
name: "shell".to_string()
}
);
assert_eq!(
resp.group,
ADBStatMapping {
id: 2000,
name: "shell".to_string()
}
);
assert_eq!(resp.atime, 0);
assert_eq!(resp.mtime, 0);
assert_eq!(resp.ctime, 1_732_807_643);
}
}