use std::fs::{self, File};
use std::io::{self, Read, Seek, SeekFrom};
use stuffr::lz4_opts::{
LEGACY_BLOCK_SIZE, LEGACY_MAGIC, MAGIC, is_skippable_magic, parse_frame_header,
};
use super::cli::{Command, STDINMARK};
use super::io::{Ended, Fail, Run};
use super::messages::{self as m, printf};
use crate::compat::host::{self, Host};
trait Input: Read + Seek {}
impl<T: Read + Seek> Input for T {}
const WINDOW: usize = 64 << 10;
const PEEK: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Frame,
Legacy,
Skippable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FrameInfo {
kind: Kind,
size: u64,
block: Option<(u8, bool)>,
checksum: bool,
content: u64,
}
impl FrameInfo {
fn known(&self) -> bool {
self.kind == Kind::Frame && self.content != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stop {
NotRecognized,
CorruptLegacy,
Trailing(u64),
MagicUnreadable,
SkipSize,
MagicOnly,
HeaderCut,
}
struct Window<R> {
r: R,
base: u64,
buf: Vec<u8>,
start: u64,
}
impl<R: Read + Seek> Window<R> {
fn new(mut r: R) -> Window<R> {
let base = r.stream_position().unwrap_or(0);
Window {
r,
base,
buf: Vec::new(),
start: 0,
}
}
fn read_at(&mut self, pos: u64, out: &mut [u8]) -> usize {
let mut n = 0;
while n < out.len() {
let p = pos.saturating_add(n as u64);
if p >= self.start && p - self.start < self.buf.len() as u64 {
let off = (p - self.start) as usize;
let k = (self.buf.len() - off).min(out.len() - n);
out[n..n + k].copy_from_slice(&self.buf[off..off + k]);
n += k;
} else if !self.fill(p) {
break;
}
}
n
}
fn fill(&mut self, p: u64) -> bool {
self.buf.clear();
if self
.r
.seek(SeekFrom::Start(self.base.saturating_add(p)))
.is_err()
{
return false;
}
self.buf.resize(WINDOW, 0);
loop {
match self.r.read(&mut self.buf) {
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Ok(0) | Err(_) => {
self.buf.clear();
return false;
}
Ok(k) => {
self.buf.truncate(k);
self.start = p;
return true;
}
}
}
}
}
struct Walker<R> {
win: Window<R>,
pos: u64,
word: [u8; 4],
frames: u64,
}
impl<R: Read + Seek> Walker<R> {
fn new(r: R) -> Walker<R> {
Walker {
win: Window::new(r),
pos: 0,
word: [0; 4],
frames: 0,
}
}
fn next_frame(&mut self) -> Result<Option<FrameInfo>, Stop> {
let start = self.pos;
let mut magic = [0u8; 4];
match self.win.read_at(start, &mut magic) {
0 if self.frames == 0 => return Err(Stop::NotRecognized),
0 => return Ok(None),
4 => {}
_ => return Err(Stop::MagicUnreadable),
}
let value = u32::from_le_bytes(magic);
let info = if value == MAGIC {
self.modern(start, magic)?
} else if value == LEGACY_MAGIC {
self.legacy(start)?
} else if is_skippable_magic(value) {
self.skippable(start)?
} else {
return Err(Stop::Trailing(start + 4));
};
self.frames += 1;
Ok(Some(info))
}
fn modern(&mut self, start: u64, magic: [u8; 4]) -> Result<FrameInfo, Stop> {
let mut head = [0u8; PEEK];
let k = self.win.read_at(start + 4, &mut head);
if k == 0 {
return Err(Stop::MagicOnly);
}
let content_size = head[0] & 8 != 0;
let dictionary = head[0] & 1 != 0;
if k < 4 {
return Err(if content_size {
Stop::HeaderCut
} else {
Stop::NotRecognized
});
}
if dictionary && !content_size {
return Err(Stop::NotRecognized);
}
let after = 3 + if content_size { 8 } else { 0 } + if dictionary { 4 } else { 0 };
if k <= after {
return Err(Stop::NotRecognized);
}
let mut full = [0u8; 4 + PEEK];
full[..4].copy_from_slice(&magic);
full[4..4 + after].copy_from_slice(&head[..after]);
let Ok(Some(h)) = parse_frame_header(&full[..4 + after]) else {
return Err(Stop::NotRecognized);
};
let mut p = start + h.header_len as u64;
let mut first = true;
loop {
let mut w = [0u8; 4];
let n = self.win.read_at(p, &mut w);
if n == 0 {
if first {
return Err(Stop::NotRecognized);
}
break;
}
self.word[..n].copy_from_slice(&w[..n]);
p += 4;
first = false;
let size = u64::from(u32::from_le_bytes(self.word) & 0x7FFF_FFFF);
if size == 0 {
if h.content_checksum {
p += 4;
}
break;
}
p += size + if h.block_checksum { 4 } else { 0 };
}
self.pos = p;
Ok(FrameInfo {
kind: Kind::Frame,
size: p - start,
block: Some((h.block_size.id(), h.block_linked)),
checksum: h.content_checksum,
content: h.content_size.unwrap_or(0),
})
}
fn legacy(&mut self, start: u64) -> Result<FrameInfo, Stop> {
let mut p = start + 4;
let mut first = true;
loop {
let mut w = [0u8; 4];
let n = self.win.read_at(p, &mut w);
if n == 0 {
if first {
return Err(Stop::NotRecognized);
}
break;
}
if n < 4 {
return Err(Stop::CorruptLegacy);
}
let v = u32::from_le_bytes(w);
if v == MAGIC || v == LEGACY_MAGIC || is_skippable_magic(v) {
if first {
return Err(Stop::NotRecognized);
}
break;
}
if v as usize > LEGACY_BLOCK_SIZE {
return Err(Stop::CorruptLegacy);
}
p += 4 + u64::from(v);
first = false;
}
self.pos = p;
Ok(FrameInfo {
kind: Kind::Legacy,
size: p - start,
block: None,
checksum: false,
content: 0,
})
}
fn skippable(&mut self, start: u64) -> Result<FrameInfo, Stop> {
let mut w = [0u8; 4];
if self.win.read_at(start + 4, &mut w) < 4 {
return Err(Stop::SkipSize);
}
let size = u32::from_le_bytes(w);
self.pos = start + 8 + u64::from(size);
Ok(FrameInfo {
kind: Kind::Skippable,
size: u64::from(size.wrapping_add(8)),
block: None,
checksum: false,
content: 0,
})
}
}
#[derive(Debug, Default)]
struct Totals {
count: u64,
kind: Option<Kind>,
kind_same: bool,
block: Option<Option<(u8, bool)>>,
block_same: bool,
known: bool,
content: u64,
}
impl Totals {
fn new() -> Totals {
Totals {
kind_same: true,
block_same: true,
known: true,
..Totals::default()
}
}
fn add(&mut self, f: &FrameInfo) {
self.count += 1;
match self.kind {
None => self.kind = Some(f.kind),
Some(k) => self.kind_same &= k == f.kind,
}
match self.block {
None => self.block = Some(f.block),
Some(b) => self.block_same &= b == f.block,
}
self.known &= f.known();
self.content = self.content.wrapping_add(f.content);
}
}
fn pad(s: &str, width: usize) -> Vec<u8> {
format!("{s:>width$}").into_bytes()
}
fn kind_cell(k: Kind) -> &'static str {
match k {
Kind::Frame => m::TYPE_FRAME,
Kind::Legacy => m::TYPE_LEGACY,
Kind::Skippable => m::TYPE_SKIPPABLE,
}
}
fn block_cell(b: Option<(u8, bool)>) -> String {
match b {
Some((id, linked)) => format!(
"{}{id}{}",
m::BLOCK_PREFIX,
if linked {
m::BLOCK_LINKED
} else {
m::BLOCK_INDEPENDENT
}
),
None => m::CELL_NONE.to_string(),
}
}
fn size_cell(n: u64) -> String {
const UNITS: [&str; 7] = ["", "K", "M", "G", "T", "P", "E"];
let mut v = n as f64;
let mut unit = 0;
while v >= 1024.0 && unit + 1 < UNITS.len() {
v /= 1024.0;
unit += 1;
}
format!("{v:.2}{}", UNITS[unit])
}
fn ratio_cell(part: u64, whole: u64) -> String {
format!("{:.2}", part as f64 / whole as f64 * 100.0)
}
fn base_name(name: &[u8]) -> &[u8] {
name.iter()
.rposition(|&b| b == b'/')
.map_or(name, |i| &name[i + 1..])
}
fn print_frame_row(run: &mut Run, index: u64, f: &FrameInfo) {
let index = pad(&index.to_string(), 10);
let kind = pad(kind_cell(f.kind), 14);
let block = pad(&block_cell(f.block), 5);
let checksum = pad(
if f.checksum {
m::CHECKSUM_XXH32
} else {
m::CELL_NONE
},
8,
);
let size = pad(&f.size.to_string(), 20);
let known = f.known();
let (content, ratio) = if known {
(f.content.to_string(), ratio_cell(f.size, f.content))
} else {
(m::CELL_NONE.to_string(), m::CELL_NONE.to_string())
};
let content = pad(&content, 20);
let ratio = pad(&ratio, 9);
let fmt = match (f.kind, known) {
(Kind::Frame, true) => m::LIST_FRAME_KNOWN,
(Kind::Frame, false) => m::LIST_FRAME_UNKNOWN,
_ => m::LIST_FRAME_OTHER,
};
printf(
run.out,
fmt,
&[&index, &kind, &block, &checksum, &size, &content, &ratio],
);
}
fn print_file_row(run: &mut Run, name: &[u8], t: &Totals, file_size: u64) {
let count = pad(&t.count.to_string(), 10);
let kind = match t.kind {
Some(k) if t.kind_same => kind_cell(k),
_ => m::CELL_NONE,
};
let kind = pad(kind, 14);
let block = match t.block {
Some(b) if t.block_same => block_cell(b),
_ => m::CELL_NONE.to_string(),
};
let block = pad(&block, 5);
let size = pad(&size_cell(file_size), 11);
let (content, ratio) = if t.known {
(size_cell(t.content), ratio_cell(file_size, t.content))
} else {
(m::CELL_NONE.to_string(), m::CELL_NONE.to_string())
};
let content = pad(&content, 13);
let ratio = pad(&ratio, 8);
let fmt = if t.known {
m::LIST_ROW_KNOWN
} else {
m::LIST_ROW_UNKNOWN
};
printf(
run.out,
fmt,
&[
&count,
&kind,
&block,
&size,
&content,
&ratio,
base_name(name),
],
);
}
fn report(run: &mut Run, name: &[u8], stop: Stop) -> Fail {
match stop {
Stop::NotRecognized => {}
Stop::CorruptLegacy => run.say(1, m::LIST_CORRUPTED_LEGACY, &[]),
Stop::Trailing(at) => {
run.say(3, m::UNDECODABLE, &[]);
run.say(3, m::AT_POSITION, &[at.to_string().as_bytes()]);
run.say(3, m::NEWLINE, &[]);
}
Stop::MagicUnreadable => return run.exm(40, m::E40_MAGIC_UNREADABLE, &[]),
Stop::SkipSize => return run.exm(42, m::E42_SKIPPABLE_SIZE, &[]),
Stop::MagicOnly => return run.exm(71, m::E71_READING, &[name]),
Stop::HeaderCut => return run.exm(72, m::E72_READING, &[name]),
}
run.say(1, m::LIST_NOT_RECOGNIZED, &[name]);
Fail::File
}
type Opened = (fs::Metadata, Result<Box<dyn Input>, io::Error>);
fn open_regular(run: &mut Run, name: &[u8]) -> Option<Opened> {
if name == STDINMARK {
return match run.host {
Host::Real => {
let f = host::stdin_file().ok()?;
let meta = f.metadata().ok().filter(fs::Metadata::is_file)?;
Some((meta, Ok(Box::new(f))))
}
Host::Sandbox(_) => None,
};
}
let path = run.host.path(name);
let meta = fs::metadata(&path).ok().filter(fs::Metadata::is_file)?;
let host = &*run.host;
let file = File::open(&path).map(|f| -> Box<dyn Input> { Box::new(host.track(&path, f)) });
Some((meta, file))
}
fn list_one(run: &mut Run, name: &[u8], index: usize, total: usize) -> Result<(), Fail> {
let verbose = run.level >= 3;
let Some((meta, file)) = open_regular(run, name) else {
run.say(1, m::LIST_NOT_REGULAR, &[name]);
return Err(Fail::File);
};
if verbose {
printf(
run.out,
m::LIST_FILE_VERBOSE,
&[
base_name(name),
index.to_string().as_bytes(),
total.to_string().as_bytes(),
],
);
printf(run.out, m::LIST_HEADER_VERBOSE, &[]);
}
let file = match file {
Ok(f) => f,
Err(e) => {
run.say(1, m::OPEN_INPUT_FAILED, &[name, &host::strerror(&e)]);
return Err(Fail::File);
}
};
let mut walker = Walker::new(file);
let mut totals = Totals::new();
loop {
match walker.next_frame() {
Ok(Some(frame)) => {
totals.add(&frame);
if verbose {
print_frame_row(run, totals.count, &frame);
}
}
Ok(None) => break,
Err(stop) => return Err(report(run, name, stop)),
}
}
if verbose {
printf(run.out, m::LIST_FILE_END, &[]);
} else {
print_file_row(run, name, &totals, meta.len());
}
Ok(())
}
pub(crate) fn list_files(
cmd: &Command,
host: &mut Host,
out: &mut dyn io::Write,
err: &mut dyn io::Write,
) -> Ended {
let mut run = Run::new(cmd, host, out, err, cmd.display_level);
if run.level < 3 {
printf(run.out, m::LIST_HEADER, &[]);
}
let mut result = Ok(());
if cmd.from_list {
let names = run.expand(&cmd.inputs);
let total = names.len();
for (i, name) in names.iter().enumerate() {
result = list_one(&mut run, name, i + 1, total);
if result.is_err() {
break;
}
}
}
match result {
Ok(()) | Err(Fail::File) => Ended {
code: i32::from(result.is_err()),
pause: cmd.pause,
},
Err(Fail::Exit(code)) => Ended { code, pause: false },
}
}
#[cfg(test)]
mod tests;