mod file;
mod file_type;
mod pex;
use std::ops::Range;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use chrono::prelude::Utc;
use chrono::{Datelike, NaiveDate, NaiveDateTime};
use lazy_regex::{Lazy, Regex};
use thiserror::Error;
pub use self::file::File;
pub use self::file_type::FileType;
pub use self::pex::{PosixPex, PosixPexQuery};
static POSIX_LS_RE: Lazy<Regex> = lazy_regex!(
r#"^([\-ld])([\-rwxsStT]{9})\s+(\d+)\s+([^ ]+)\s+([^ ]+)\s+(\d+)\s+([^ ]+\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#
);
static DOS_LS_RE: Lazy<Regex> =
lazy_regex!(r#"^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}\s*[AP]M)\s+(<DIR>)?([\d,]*)\s+(.+)$"#);
pub type ParseResult<T> = Result<T, ParseError>;
#[derive(Debug, Error, Eq, PartialEq)]
pub enum ParseError {
#[error("Syntax error: invalid line")]
SyntaxError,
#[error("Invalid date")]
InvalidDate,
#[error("Bad file size")]
BadSize,
}
pub struct ListParser;
impl ListParser {
pub fn parse_mlsd(line: &str) -> ParseResult<File> {
Self::parse_mlsx(line)
}
pub fn parse_mlst(line: &str) -> ParseResult<File> {
Self::parse_mlsx(line)
}
fn parse_mlsx(line: &str) -> ParseResult<File> {
let tokens = line.split(';').collect::<Vec<&str>>();
if tokens.is_empty() {
return Err(ParseError::SyntaxError);
}
let mut f = File {
name: String::default(),
file_type: FileType::File,
size: 0,
modified: SystemTime::UNIX_EPOCH,
uid: None,
gid: None,
posix_pex: (
PosixPex::from(0o7),
PosixPex::from(0o7),
PosixPex::from(0o7),
),
};
for token in tokens.iter() {
let mut parts = token.split('=');
let key = match parts.next() {
Some(k) => k,
None => continue,
};
let value = match parts.next() {
Some(v) => v,
None => continue,
};
match key.to_lowercase().as_str() {
"type" => {
f.file_type = match value.to_lowercase().as_str() {
"dir" | "cdir" | "pdir" => FileType::Directory,
"file" => FileType::File,
"link" => FileType::Symlink(PathBuf::default()),
_ => return Err(ParseError::SyntaxError),
};
}
"size" => {
f.size = value.parse::<usize>().map_err(|_| ParseError::BadSize)?;
}
"modify" => {
f.modified = Self::parse_mlsx_time(value)?;
}
"unix.uid" => {
f.uid = value.parse::<u32>().ok();
}
"unix.gid" => {
f.gid = value.parse::<u32>().ok();
}
"unix.mode" => {
if value.len() != 3 && value.len() != 4 {
return Err(ParseError::SyntaxError);
}
let mode_str = &value[value.len() - 3..];
let modes = mode_str
.chars()
.map(|c| c.to_digit(8).unwrap_or(0))
.collect::<Vec<u32>>();
f.posix_pex = (
PosixPex::from(modes[0] as u8),
PosixPex::from(modes[1] as u8),
PosixPex::from(modes[2] as u8),
);
}
_ => continue,
}
}
f.name = tokens.last().unwrap().trim_start().to_string();
Ok(f)
}
pub fn parse_posix(line: &str) -> ParseResult<File> {
match POSIX_LS_RE.captures(line) {
Some(metadata) => {
trace!("Parsed POSIX line {}", line);
if metadata.len() < 8 {
trace!("Bad syntax for posix line");
return Err(ParseError::SyntaxError);
}
let file_type: FileType = match metadata.get(1).unwrap().as_str() {
"-" => FileType::File,
"d" => FileType::Directory,
"l" => FileType::Symlink(PathBuf::default()),
_ => return Err(ParseError::SyntaxError), };
let pex = |range: Range<usize>| {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() {
match c {
'-' | 'S' | 'T' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
let posix_pex: (PosixPex, PosixPex, PosixPex) = (
PosixPex::from(pex(0..3)),
PosixPex::from(pex(3..6)),
PosixPex::from(pex(6..9)),
);
let modified: SystemTime = Self::parse_lstime(
metadata.get(7).unwrap().as_str().trim(),
"%b %d %Y",
"%b %d %H:%M",
)?;
let gid: Option<u32> = metadata.get(5).unwrap().as_str().trim().parse::<u32>().ok();
let uid: Option<u32> = metadata.get(4).unwrap().as_str().trim().parse::<u32>().ok();
let size: usize = metadata
.get(6)
.unwrap()
.as_str()
.parse::<usize>()
.map_err(|_| ParseError::BadSize)?;
let (name, symlink_path): (String, Option<PathBuf>) = match file_type.is_symlink() {
true => Self::get_name_and_link(metadata.get(8).unwrap().as_str()),
false => (String::from(metadata.get(8).unwrap().as_str()), None),
};
let file_type: FileType = match symlink_path {
Some(p) => FileType::Symlink(p),
None => file_type,
};
trace!(
"Found file with name {}, type: {:?}, size: {}, uid: {:?}, gid: {:?}, pex: {:?}",
name, file_type, size, uid, gid, posix_pex
);
Ok(File {
name,
file_type,
size,
modified,
uid,
gid,
posix_pex,
})
}
None => Err(ParseError::SyntaxError),
}
}
pub fn parse_dos(line: &str) -> ParseResult<File> {
match DOS_LS_RE.captures(line) {
Some(metadata) => {
trace!("Parsed DOS line {}", line);
if metadata.len() < 5 {
return Err(ParseError::SyntaxError);
}
let modified: SystemTime = Self::parse_dostime(metadata.get(1).unwrap().as_str())?;
let file_type: FileType = match metadata.get(2).is_some() {
true => FileType::Directory,
false => FileType::File,
};
let size: usize = match file_type.is_directory() {
true => 0, false => match metadata.get(3) {
Some(val) => val
.as_str()
.replace(',', "")
.parse::<usize>()
.map_err(|_| ParseError::BadSize)?,
None => 0,
},
};
let name: String = String::from(metadata.get(4).unwrap().as_str());
trace!(
"Found file with name {}, type: {:?}, size: {}",
name, file_type, size,
);
Ok(File {
name,
file_type,
size,
modified,
uid: None,
gid: None,
posix_pex: (
PosixPex::default(),
PosixPex::default(),
PosixPex::default(),
),
})
}
None => Err(ParseError::SyntaxError), }
}
fn get_name_and_link(token: &str) -> (String, Option<PathBuf>) {
let tokens: Vec<&str> = token.split(" -> ").collect();
let filename: String = String::from(*tokens.first().unwrap());
let symlink: Option<PathBuf> = tokens.get(1).map(PathBuf::from);
(filename, symlink)
}
fn parse_mlsx_time(tm: &str) -> Result<SystemTime, ParseError> {
NaiveDateTime::parse_from_str(tm, "%Y%m%d%H%M%S")
.map(|dt| {
SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(dt.and_utc().timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH)
})
.map_err(|_| ParseError::InvalidDate)
}
fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemTime, ParseError> {
let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) {
Ok(date) => {
date.and_hms_opt(0, 0, 0).unwrap()
}
Err(_) => {
let now = Utc::now();
let this_year: i32 = now.year();
let date_time_str: String = format!("{tm} {this_year}");
let mut dt = NaiveDateTime::parse_from_str(
date_time_str.as_ref(),
format!("{fmt_hours} %Y").as_ref(),
)
.map_err(|_| ParseError::InvalidDate)?;
if dt.and_utc().timestamp() - now.timestamp() > 180 * 24 * 3600 {
let date_time_str: String = format!("{tm} {}", this_year - 1);
dt = NaiveDateTime::parse_from_str(
date_time_str.as_ref(),
format!("{fmt_hours} %Y").as_ref(),
)
.map_err(|_| ParseError::InvalidDate)?;
}
dt
}
};
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
Ok(sys_time
.checked_add(Duration::from_secs(datetime.and_utc().timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH))
}
fn parse_dostime(tm: &str) -> Result<SystemTime, ParseError> {
NaiveDateTime::parse_from_str(tm, "%m-%d-%y %I:%M%p")
.or_else(|_| NaiveDateTime::parse_from_str(tm, "%m-%d-%y %I:%M %p"))
.map(|dt| {
SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(dt.and_utc().timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH)
})
.map_err(|_| ParseError::InvalidDate)
}
}
#[cfg(test)]
mod tests {
use chrono::DateTime;
use super::*;
#[test]
fn parse_posix_line() {
let file = ListParser::parse_posix("-rw-rw-r-- 1 0 1 8192 Nov 5 2018 omar.txt").unwrap();
pretty_assertions::assert_eq!(file.name(), "omar.txt");
pretty_assertions::assert_eq!(file.size, 8192);
pretty_assertions::assert_eq!(file.is_file(), true);
pretty_assertions::assert_eq!(file.uid, Some(0));
pretty_assertions::assert_eq!(file.gid, Some(1));
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(
file.modified()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
let file =
ListParser::parse_posix("drwxrwxr-x 1 root dialout 4096 Nov 5 2018 provola").unwrap();
pretty_assertions::assert_eq!(file.name(), "provola");
pretty_assertions::assert_eq!(file.size, 4096);
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.uid, None);
pretty_assertions::assert_eq!(file.gid, None);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(
file.modified()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
let file: File = ListParser::parse_posix(
"drws------ 2 u-redacted g-redacted 3864 Feb 17 2023 sas",
)
.unwrap();
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), false);
let file: File = ListParser::parse_posix(
"drwS------ 2 u-redacted g-redacted 3864 Feb 17 2023 sas",
)
.unwrap();
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), false);
let file: File = ListParser::parse_posix(
"drwx--s--- 2 u-redacted g-redacted 3864 Feb 17 2023 sas",
)
.unwrap();
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), false);
let file: File = ListParser::parse_posix(
"drwx--S--- 2 u-redacted g-redacted 3864 Feb 17 2023 sas",
)
.unwrap();
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), false);
let file: File = ListParser::parse_posix(
"drwx-----t 2 u-redacted g-redacted 3864 Feb 17 2023 sas",
)
.unwrap();
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), true);
let file: File = ListParser::parse_posix(
"drwx--S--T 2 u-redacted g-redacted 3864 Feb 17 2023 sas",
)
.unwrap();
pretty_assertions::assert_eq!(file.is_directory(), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), false);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), false);
pretty_assertions::assert_eq!(
ListParser::parse_posix("drwxrwxr-x 1 0 9 Nov 5 2018 docs").unwrap_err(),
ParseError::SyntaxError
);
pretty_assertions::assert_eq!(
ListParser::parse_posix("drwxrwxr-x 1 root dialout 4096 Nov 31 2018 provola")
.unwrap_err(),
ParseError::InvalidDate
);
}
#[test]
fn should_parse_utf8_names_in_ls_output() {
assert!(
ListParser::parse_posix("-rw-rw-r-- 1 омар www-data 8192 Nov 5 2018 фообар.txt")
.is_ok()
);
}
#[test]
fn parse_dos_line() {
let file: File = ListParser::parse_dos("04-08-14 03:09PM 8192 omar.txt")
.ok()
.unwrap();
pretty_assertions::assert_eq!(file.name(), "omar.txt");
pretty_assertions::assert_eq!(file.size, 8192);
assert!(file.is_file());
pretty_assertions::assert_eq!(file.gid, None);
pretty_assertions::assert_eq!(file.uid, None);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(
file.modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1396969740)
);
let dir: File = ListParser::parse_dos("04-08-14 03:09PM <DIR> docs").unwrap();
pretty_assertions::assert_eq!(dir.name(), "docs");
assert!(dir.is_directory());
pretty_assertions::assert_eq!(dir.uid, None);
pretty_assertions::assert_eq!(dir.gid, None);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(
dir.modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1396969740)
);
pretty_assertions::assert_eq!(
ListParser::parse_dos("-08-14 03:09PM <DIR> docs")
.err()
.unwrap(),
ParseError::SyntaxError
);
pretty_assertions::assert_eq!(
ListParser::parse_dos("34-08-14 03:09PM <DIR> docs")
.err()
.unwrap(),
ParseError::InvalidDate
);
pretty_assertions::assert_eq!(
ListParser::parse_dos("04-08-14 03:09PM OMAR docs")
.err()
.unwrap(),
ParseError::BadSize
);
}
#[test]
fn parse_dos_line_with_space_before_ampm() {
let file: File = ListParser::parse_dos("04-08-14 03:09 PM 1234 readme.txt")
.ok()
.unwrap();
pretty_assertions::assert_eq!(file.name(), "readme.txt");
pretty_assertions::assert_eq!(file.size, 1234);
assert!(file.is_file());
let file: File = ListParser::parse_dos("04-08-14 10:30 AM <DIR> somedir")
.ok()
.unwrap();
pretty_assertions::assert_eq!(file.name(), "somedir");
assert!(file.is_directory());
let with_space = ListParser::parse_dos("04-08-14 03:09 PM 1234 readme.txt")
.ok()
.unwrap();
let without_space = ListParser::parse_dos("04-08-14 03:09PM 1234 readme.txt")
.ok()
.unwrap();
pretty_assertions::assert_eq!(with_space.modified, without_space.modified);
}
#[test]
fn parse_dos_line_with_comma_separated_size() {
let file: File = ListParser::parse_dos("04-08-14 03:09PM 1,234 readme.txt")
.ok()
.unwrap();
pretty_assertions::assert_eq!(file.name(), "readme.txt");
pretty_assertions::assert_eq!(file.size, 1234);
assert!(file.is_file());
let file: File = ListParser::parse_dos("04-08-14 03:09PM 1,234,567 bigfile.bin")
.ok()
.unwrap();
pretty_assertions::assert_eq!(file.name(), "bigfile.bin");
pretty_assertions::assert_eq!(file.size, 1234567);
assert!(file.is_file());
}
#[test]
fn test_should_parse_name_starting_with_tricky_numbers() {
let file = ListParser::parse_posix(
"-r--r--r-- 1 23 23 1234567 Jan 1 2000 01 1234 foo.mp3",
)
.unwrap();
pretty_assertions::assert_eq!(file.name(), "01 1234 foo.mp3");
pretty_assertions::assert_eq!(file.size, 1234567);
pretty_assertions::assert_eq!(
file.modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(946684800)
);
}
#[test]
fn parse_lstime() {
pretty_assertions::assert_eq!(
fmt_time(
ListParser::parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap(),
"%m %d %M"
)
.as_str(),
"11 05 32"
);
pretty_assertions::assert_eq!(
fmt_time(
ListParser::parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap(),
"%m %d %M"
)
.as_str(),
"12 02 32"
);
pretty_assertions::assert_eq!(
ListParser::parse_lstime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
pretty_assertions::assert_eq!(
ListParser::parse_lstime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1521331200)
);
assert!(ListParser::parse_lstime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
assert!(ListParser::parse_lstime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
assert!(ListParser::parse_lstime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
}
#[test]
fn parse_lstime_should_adjust_year_for_future_dates() {
use chrono::Months;
let now = Utc::now();
let this_year = now.year();
let future_date = now.naive_utc().date() + Months::new(8);
let month_abbr = future_date.format("%b").to_string();
let time_str = format!("{month_abbr} 15 12:00");
let result = ListParser::parse_lstime(&time_str, "%b %d %Y", "%b %d %H:%M").unwrap();
let result_dt: DateTime<Utc> = result.into();
pretty_assertions::assert_eq!(result_dt.year(), this_year - 1);
pretty_assertions::assert_eq!(result_dt.month(), future_date.month());
pretty_assertions::assert_eq!(result_dt.day(), 15);
}
#[test]
fn parse_lstime_should_not_adjust_year_for_near_future_dates() {
use chrono::Months;
let now = Utc::now();
let this_year = now.year();
let near_date = now.naive_utc().date() + Months::new(2);
let month_abbr = near_date.format("%b").to_string();
let time_str = format!("{month_abbr} 15 12:00");
let result = ListParser::parse_lstime(&time_str, "%b %d %Y", "%b %d %H:%M").unwrap();
let result_dt: DateTime<Utc> = result.into();
pretty_assertions::assert_eq!(result_dt.year(), this_year);
pretty_assertions::assert_eq!(result_dt.month(), near_date.month());
pretty_assertions::assert_eq!(result_dt.day(), 15);
}
#[test]
fn parse_lstime_should_not_adjust_year_for_recent_past_dates() {
use chrono::Months;
let now = Utc::now();
let this_year = now.year();
let past_date = now.naive_utc().date() - Months::new(1);
let month_abbr = past_date.format("%b").to_string();
let time_str = format!("{month_abbr} 15 12:00");
let result = ListParser::parse_lstime(&time_str, "%b %d %Y", "%b %d %H:%M").unwrap();
let result_dt: DateTime<Utc> = result.into();
pretty_assertions::assert_eq!(result_dt.year(), this_year);
pretty_assertions::assert_eq!(result_dt.month(), past_date.month());
}
#[test]
fn parse_posix_line_should_adjust_year_for_future_dates() {
use chrono::Months;
let now = Utc::now();
let this_year = now.year();
let future_date = now.naive_utc().date() + Months::new(8);
let month_abbr = future_date.format("%b").to_string();
let line = format!("-rw-r--r-- 1 user group 1234 {month_abbr} 15 10:30 example.txt");
let file = ListParser::parse_posix(&line).unwrap();
pretty_assertions::assert_eq!(file.name(), "example.txt");
pretty_assertions::assert_eq!(file.size, 1234);
let result_dt: DateTime<Utc> = file.modified.into();
pretty_assertions::assert_eq!(result_dt.year(), this_year - 1);
pretty_assertions::assert_eq!(result_dt.month(), future_date.month());
}
#[test]
fn parse_dostime() {
pretty_assertions::assert_eq!(
ListParser::parse_dostime("04-08-14 03:09PM")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1396969740)
);
pretty_assertions::assert_eq!(
ListParser::parse_dostime("04-08-14 03:09 PM")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1396969740)
);
pretty_assertions::assert_eq!(
ListParser::parse_dostime("04-08-14 03:09AM")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1396926540)
);
pretty_assertions::assert_eq!(
ListParser::parse_dostime("04-08-14 03:09 AM")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1396926540)
);
assert!(ListParser::parse_dostime("04-08-14").is_err());
}
#[test]
fn test_parse_mlsx_line() {
let file =
ListParser::parse_mlsd("type=file;size=8192;modify=20181105163248; omar.txt").unwrap();
pretty_assertions::assert_eq!(file.name(), "omar.txt");
pretty_assertions::assert_eq!(file.size, 8192);
assert!(file.is_file());
pretty_assertions::assert_eq!(file.gid, None);
pretty_assertions::assert_eq!(file.uid, None);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Owner), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Group), true);
pretty_assertions::assert_eq!(file.can_read(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_write(PosixPexQuery::Others), true);
pretty_assertions::assert_eq!(file.can_execute(PosixPexQuery::Others), true);
let file =
ListParser::parse_mlsd("type=dir;size=4096;modify=20181105163248; docs").unwrap();
pretty_assertions::assert_eq!(file.name(), "docs");
assert!(file.is_directory());
let file = ListParser::parse_mlsd("type=cdir;size=4096;modify=20181105163248; .").unwrap();
pretty_assertions::assert_eq!(file.name(), ".");
assert!(file.is_directory());
let file = ListParser::parse_mlsd("type=pdir;size=4096;modify=20181105163248; ..").unwrap();
pretty_assertions::assert_eq!(file.name(), "..");
assert!(file.is_directory());
let file = ListParser::parse_mlsd(
"type=file;size=4096;modify=20181105163248;unix.mode=644; omar.txt",
)
.unwrap();
pretty_assertions::assert_eq!(
file.posix_pex,
(PosixPex::from(6), PosixPex::from(4), PosixPex::from(4))
);
let file = ListParser::parse_mlsd(
"type=file;size=4096;modify=20181105163248;unix.mode=0644; omar.txt",
)
.unwrap();
pretty_assertions::assert_eq!(
file.posix_pex,
(PosixPex::from(6), PosixPex::from(4), PosixPex::from(4))
);
let file = ListParser::parse_mlsd(
"type=file;size=4096;modify=20181105163248;unix.mode=0755; script.sh",
)
.unwrap();
pretty_assertions::assert_eq!(
file.posix_pex,
(PosixPex::from(7), PosixPex::from(5), PosixPex::from(5))
);
let file = ListParser::parse_mlsd(
"type=file;size=4096;modify=20181105163248;unix.mode=755; script.sh",
)
.unwrap();
pretty_assertions::assert_eq!(
file.posix_pex,
(PosixPex::from(7), PosixPex::from(5), PosixPex::from(5))
);
assert!(
ListParser::parse_mlsd(
"type=file;size=4096;modify=20181105163248;unix.mode=64; bad.txt",
)
.is_err()
);
assert!(
ListParser::parse_mlsd(
"type=file;size=4096;modify=20181105163248;unix.mode=06444; bad.txt",
)
.is_err()
);
}
#[test]
fn parse_mlsx_with_link_type() {
let file =
ListParser::parse_mlsd("type=link;size=0;modify=20181105163248; mylink").unwrap();
pretty_assertions::assert_eq!(file.name(), "mylink");
assert!(file.is_symlink());
}
#[test]
fn parse_mlsx_with_uid_gid() {
let file = ListParser::parse_mlsd(
"type=file;size=1024;modify=20201019151930;unix.uid=1000;unix.gid=2000; data.bin",
)
.unwrap();
pretty_assertions::assert_eq!(file.name(), "data.bin");
pretty_assertions::assert_eq!(file.uid, Some(1000));
pretty_assertions::assert_eq!(file.gid, Some(2000));
}
#[test]
fn parse_mlsx_with_invalid_type() {
assert!(
ListParser::parse_mlsd("type=unknown;size=0;modify=20181105163248; bad.txt").is_err()
);
}
#[test]
fn parse_mlsx_with_invalid_size() {
assert!(
ListParser::parse_mlsd("type=file;size=notanumber;modify=20181105163248; bad.txt")
.is_err()
);
}
#[test]
fn parse_mlsx_with_invalid_modify() {
assert!(ListParser::parse_mlsd("type=file;size=0;modify=notadate; bad.txt").is_err());
}
#[test]
fn parse_posix_symlink() {
let file =
ListParser::parse_posix("lrwxrwxrwx 1 root root 9 Nov 5 2018 link -> /tmp/target")
.unwrap();
pretty_assertions::assert_eq!(file.name(), "link");
assert!(file.is_symlink());
pretty_assertions::assert_eq!(file.symlink(), Some(std::path::Path::new("/tmp/target")));
}
#[test]
fn get_name_and_link() {
pretty_assertions::assert_eq!(
ListParser::get_name_and_link("Cargo.toml"),
(String::from("Cargo.toml"), None)
);
pretty_assertions::assert_eq!(
ListParser::get_name_and_link("Cargo -> Cargo.toml"),
(String::from("Cargo"), Some(PathBuf::from("Cargo.toml")))
);
}
fn fmt_time(time: SystemTime, fmt: &str) -> String {
let datetime: DateTime<Utc> = time.into();
format!("{}", datetime.format(fmt))
}
}