use std::cmp::Ordering;
use std::convert::TryInto;
use std::fmt;
use anyhow::Context;
use chrono::{Local, TimeZone};
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use crate::varint::ReadVarint;
use crate::Result;
const STATUS_REPEAT_MODE: u8 = 0x02;
const STATUS_REPEAT_PARTIAL_NAME: u8 = 0x20;
const STATUS_LONG_NAME: u8 = 0x40;
const STATUS_REPEAT_MTIME: u8 = 0x80;
type ByteString = Vec<u8>;
#[derive(Debug, PartialEq, Eq)]
pub struct FileEntry {
name: Vec<u8>,
pub file_len: u64,
pub mode: u32,
mtime: u32,
link_target: Option<ByteString>,
}
impl FileEntry {
pub fn name_bytes(&self) -> &[u8] {
&self.name
}
pub fn name_lossy_string(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.name)
}
pub fn is_file(&self) -> bool {
unix_mode::is_file(self.mode)
}
pub fn is_dir(&self) -> bool {
unix_mode::is_dir(self.mode)
}
pub fn is_symlink(&self) -> bool {
unix_mode::is_symlink(self.mode)
}
pub fn unix_mtime(&self) -> u32 {
self.mtime
}
pub fn mtime(&self) -> chrono::DateTime<Local> {
Local.timestamp(self.mtime as i64, 0)
}
}
impl fmt::Display for FileEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:08} {:11} {:19} {}",
unix_mode::to_string(self.mode),
self.file_len,
self.mtime().format("%Y-%m-%d %H:%M:%S"),
self.name_lossy_string(),
)
}
}
pub type FileList = Vec<FileEntry>;
pub(crate) fn read_file_list(r: &mut ReadVarint) -> Result<FileList> {
let mut v: Vec<FileEntry> = Vec::new();
while let Some(entry) = receive_file_entry(r, v.last())? {
v.push(entry)
}
debug!("End of file list");
Ok(v)
}
fn receive_file_entry(
r: &mut ReadVarint,
previous: Option<&FileEntry>,
) -> Result<Option<FileEntry>> {
let status = r
.read_u8()
.context("Failed to read file entry status byte")?;
trace!("File list status {:#x}", status);
if status == 0 {
return Ok(None);
}
let inherit_name_bytes = if (status & STATUS_REPEAT_PARTIAL_NAME) != 0 {
r.read_u8().context("Failed to read inherited name bytes")? as usize
} else {
0
};
let name_len = if status & STATUS_LONG_NAME != 0 {
r.read_i32()? as usize
} else {
r.read_u8()? as usize
};
let mut name = r.read_byte_string(name_len)?;
if inherit_name_bytes > 0 {
let mut new_name = previous.unwrap().name.clone();
new_name.truncate(inherit_name_bytes);
new_name.append(&mut name);
name = new_name;
}
trace!(" filename: {:?}", String::from_utf8_lossy(&name));
assert!(!name.is_empty());
let file_len: u64 = r
.read_i64()?
.try_into()
.context("Received negative file_len")?;
trace!(" file_len: {}", file_len);
let mtime = if status & STATUS_REPEAT_MTIME == 0 {
r.read_i32()? as u32
} else {
previous.unwrap().mtime
};
trace!(" mtime: {}", mtime);
let mode = if status & STATUS_REPEAT_MODE == 0 {
r.read_i32()? as u32
} else {
previous.unwrap().mode
};
trace!(" mode: {:#o}", mode);
Ok(Some(FileEntry {
name,
file_len,
mtime,
mode,
link_target: None,
}))
}
fn filename_compare_27(a: &[u8], b: &[u8]) -> Ordering {
a.cmp(&b)
}
pub(crate) fn sort(file_list: &mut [FileEntry]) {
file_list.sort_by(|a, b| filename_compare_27(&a.name, &b.name));
debug!("File list sort done");
for (i, entry) in file_list.iter().enumerate() {
debug!("[{:8}] {:?}", i, entry.name_lossy_string())
}
}
#[cfg(test)]
mod test {
use super::*;
use regex::Regex;
#[test]
fn file_entry_display_like_ls() {
let entry = FileEntry {
mode: 0o0040750,
file_len: 420,
mtime: 1588429517,
name: b"rsyn".to_vec(),
link_target: None,
};
let entry_display = format!("{}", entry);
assert!(
Regex::new(r"drwxr-x--- 420 2020-05-0[123] \d\d:\d\d:17 rsyn")
.unwrap()
.is_match(&entry_display),
"{:?} doesn't match expected format",
entry_display
);
}
#[test]
fn ordering_examples() {
const EXAMPLE: &[&[u8]] = &[
b"./",
b".git/",
b".git/HEAD",
b".github/",
b".github/workflows/",
b".github/workflows/rust.yml",
b".gitignore",
b"CONTRIBUTING.md",
b"src/",
b"src/lib.rs",
];
for (i, a) in EXAMPLE.iter().enumerate() {
for (j, b) in EXAMPLE.iter().enumerate() {
assert_eq!(filename_compare_27(a, b), i.cmp(&j))
}
}
}
}