use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use stuffr::probe::{Chain, PROBE_LEN, identify_strict, read_tail};
use stuffr::{DecodeOpts, Error, FileSource, Registry, Source};
#[derive(Debug)]
pub enum Identified {
Openable(Chain),
NotOpenable,
}
fn read_prefix(mut f: impl Read) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; PROBE_LEN];
let mut n = 0;
while n < PROBE_LEN {
match f.read(&mut buf[n..]) {
Ok(0) => break,
Ok(k) => n += k,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
buf.truncate(n);
Ok(buf)
}
#[cfg(any(target_os = "macos", test))]
const SF_DATALESS: u32 = 0x4000_0000;
#[cfg(any(target_os = "macos", test))]
fn flags_are_dataless(flags: u32) -> bool {
flags & SF_DATALESS != 0
}
pub fn is_dataless(md: &std::fs::Metadata) -> bool {
#[cfg(target_os = "macos")]
{
use std::os::macos::fs::MetadataExt;
flags_are_dataless(md.st_flags())
}
#[cfg(not(target_os = "macos"))]
{
let _ = md;
false
}
}
pub fn identify(reg: &Registry, path: &Path) -> io::Result<Identified> {
let mut src = FileSource::from_file(File::open(path)?).map_err(into_io)?;
identify_with_reader(reg, path, &mut src)
}
fn into_io(e: Error) -> io::Error {
match e {
Error::Io(e) => e,
other => io::Error::other(other.to_string()),
}
}
pub(crate) fn identify_with_reader(
reg: &Registry,
path: &Path,
src: &mut dyn Source,
) -> io::Result<Identified> {
let tail = read_tail(src).map_err(into_io)?;
let prefix = read_prefix(&mut *src)?;
let opts = DecodeOpts {
memory_limit: Some(stuffr::default_memory_limit()),
..Default::default()
};
match identify_strict(reg, Some(path), &prefix, tail.as_deref(), &opts) {
None | Some(Chain::Raw) => Ok(Identified::NotOpenable),
Some(chain) => Ok(Identified::Openable(chain)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_dataless_flag_is_bit_0x40000000_alone() {
assert!(flags_are_dataless(0x4000_0000));
assert!(flags_are_dataless(0x4000_0000 | 0x8000 | 0x1_0000));
assert!(!flags_are_dataless(0));
assert!(!flags_are_dataless(
0x8000 | 0x1_0000 | 0x2000_0000 | 0x8000_0000
));
}
#[test]
fn an_ordinary_file_is_not_dataless() {
let md = std::fs::metadata(env!("CARGO_MANIFEST_DIR")).unwrap();
assert!(!is_dataless(&md));
}
use std::path::PathBuf;
struct Dir(PathBuf);
impl Dir {
fn new() -> Dir {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!(
"stuffr-ls-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
Dir(p)
}
fn file(&self, name: &str, bytes: &[u8]) -> PathBuf {
let p = self.0.join(name);
std::fs::write(&p, bytes).unwrap();
p
}
}
impl Drop for Dir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
const GZIP: &[u8] = &[
0x1f, 0x8b, 0x08, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
fn tar_gz() -> Vec<u8> {
use std::io::Write;
let mut tar = tar::Builder::new(Vec::new());
let mut h = tar::Header::new_ustar();
h.set_size(5);
h.set_mode(0o644);
tar.append_data(&mut h, "a.txt", &b"alpha"[..]).unwrap();
let tar = tar.into_inner().unwrap();
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
enc.write_all(&tar).unwrap();
enc.finish().unwrap()
}
fn describe(path: &Path) -> Option<String> {
match identify(stuffr::registry(), path).unwrap() {
Identified::Openable(c) => Some(c.describe()),
Identified::NotOpenable => None,
}
}
#[test]
fn gzip_without_extension_is_openable() {
let d = Dir::new();
let p = d.file("blob", GZIP);
assert_eq!(describe(&p).as_deref(), Some("gzip"));
}
#[test]
fn tar_gz_named_reads_tar_over_gzip() {
let d = Dir::new();
let p = d.file("x.tar.gz", &tar_gz());
assert_eq!(describe(&p).as_deref(), Some("tar over gzip"));
}
#[test]
fn tar_gz_named_empty_gzip_is_gzip() {
let d = Dir::new();
let p = d.file("x.tar.gz", GZIP);
assert_eq!(describe(&p).as_deref(), Some("gzip"));
}
#[test]
fn a_bare_gzip_header_is_not_openable() {
let d = Dir::new();
let p = d.file("blob.gz", &GZIP[..10]);
assert_eq!(describe(&p), None);
}
#[test]
fn html_named_zip_is_not_openable() {
let d = Dir::new();
let p = d.file("page.zip", b"<html><body>not a zip</body></html>");
assert_eq!(describe(&p), None);
}
#[test]
fn br_by_extension_is_openable() {
use stuffr::ops::{self, CompressOpts, Input, Output};
let d = Dir::new();
let src = d.file("plain.txt", b"hello hello hello hello brotli");
let dst = d.0.join("out.br");
ops::compress(
Input::Path(src),
Output::Path(dst.clone()),
&CompressOpts::default(),
)
.unwrap();
assert_eq!(describe(&dst).as_deref(), Some("brotli"));
}
#[test]
fn plain_text_is_not_openable() {
let d = Dir::new();
let p = d.file("notes.txt", b"just some text\n");
assert_eq!(describe(&p), None);
}
#[test]
fn empty_file_is_not_openable() {
let d = Dir::new();
let p = d.file("empty", b"");
assert_eq!(describe(&p), None);
}
#[test]
fn missing_file_is_an_io_error() {
let d = Dir::new();
let e = identify(stuffr::registry(), &d.0.join("nope")).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::NotFound);
}
}