use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use stuffr::probe::{Chain, PROBE_LEN, resolve_chain};
use stuffr::{Error, Registry};
#[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)
}
fn magicless_by_extension(reg: &Registry, path: &Path) -> bool {
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
return false;
};
reg.by_extension(ext).is_some_and(|id| !reg.has_magic(id))
}
#[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> {
identify_with_reader(reg, path, File::open(path)?)
}
pub(crate) fn identify_with_reader(
reg: &Registry,
path: &Path,
reader: impl Read,
) -> io::Result<Identified> {
let prefix = read_prefix(reader)?;
let worth_resolving = !reg.match_magic(&prefix).is_empty() || magicless_by_extension(reg, path);
if !worth_resolving {
return Ok(Identified::NotOpenable);
}
match resolve_chain(reg, Some(path), &prefix) {
Ok(Chain::Raw) => Ok(Identified::NotOpenable),
Ok(chain) => Ok(Identified::Openable(chain)),
Err(
Error::AmbiguousFormat { .. }
| Error::FormatNotEnabled(_)
| Error::UnknownFormat { .. },
) => Ok(Identified::NotOpenable),
Err(Error::Io(e)) => Err(e),
Err(other) => Err(io::Error::other(other.to_string())),
}
}
#[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];
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", GZIP);
assert_eq!(describe(&p).as_deref(), Some("tar over gzip"));
}
#[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);
}
}