bindet 0.3.2

Fast file type detection
Documentation
use bindet::{detect, detect_at_start, FileTypeMatches};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;
use std::fs::{read_dir, OpenOptions};
use std::io::{BufReader, BufWriter, Error, Write};
use std::path::{Path, PathBuf};

fn generate<P: AsRef<Path>>(path: P, size: usize) {
    if path.as_ref().exists() {
        return;
    }

    let mut file = OpenOptions::new()
        .write(true)
        .create(true)
        .open(path)
        .expect("failed to write random data");

    let mut writer = BufWriter::new(&mut file);

    let mut rng = rand::thread_rng();
    let mut buffer = [0u8; 8096];
    let mut total = 0;

    while total < size {
        let write_size = std::cmp::min(size - total, buffer.len());
        let buffer = &mut buffer[..write_size];
        rng.fill(buffer);
        writer.write(buffer).expect("failed to write random data");

        total += write_size;
    }
}

fn list_files() -> impl Iterator<Item = PathBuf> {
    read_dir("files/")
        .expect("failed to read files directory")
        .map(|entry| entry.expect("failed to read entry").path())
        .filter(|p| {
            p.extension().map_or(false, |e| {
                e.to_str().map_or(false, |s| s != "big" && s != "asc")
            })
        })
}

fn test_detect<P>(path: P) -> Result<Option<FileTypeMatches>, Error>
where
    P: AsRef<Path>,
{
    let file = OpenOptions::new().read(true).open(path).unwrap();

    let buf = BufReader::new(file);

    detect(buf)
}

fn test_fast_detect<P>(path: P) -> Result<Option<FileTypeMatches>, Error>
where
    P: AsRef<Path>,
{
    let file = OpenOptions::new().read(true).open(path).unwrap();

    let buf = BufReader::new(file);

    detect_at_start(buf)
}

fn detect_rar_sfx() -> Result<Option<FileTypeMatches>, Error> {
    test_detect(black_box("files/hello-world.exe"))
}

fn detect_2mib_sfx() -> Result<Option<FileTypeMatches>, Error> {
    test_detect(black_box("files/2mib.exe"))
}

fn detect_fast_2mib_sfx() -> Result<Option<FileTypeMatches>, Error> {
    test_fast_detect(black_box("files/2mib.exe"))
}

fn detect_fast_rar_sfx() -> Result<Option<FileTypeMatches>, Error> {
    test_fast_detect(black_box("files/hello-world.exe"))
}

fn criterion_benchmark(c: &mut Criterion) {
    {
        let mut group = c.benchmark_group("sfx");
        group.bench_function("sfx", |b| b.iter(detect_rar_sfx));
        group.bench_function("start-of-sfx", |b| b.iter(detect_fast_rar_sfx));
        group.bench_function("2mib-sfx", |b| b.iter(detect_2mib_sfx));
        group.bench_function("start-of-2mib-sfx", |b| b.iter(detect_fast_2mib_sfx));

        generate("files/1gib.data.big", 1 << 30);
        generate("files/100mib.data.big", 100 << 20);

        group.bench_function("1gib", |b| {
            b.iter(|| test_detect(black_box("files/1gib.data.big")))
        });
        group.bench_function("start-of-1gib", |b| {
            b.iter(|| test_fast_detect(black_box("files/1gib.data.big")))
        });

        group.bench_function("100mib", |b| {
            b.iter(|| test_detect(black_box("files/100mib.data.big")))
        });
        group.bench_function("start-of-100mib", |b| {
            b.iter(|| test_fast_detect(black_box("files/100mib.data.big")))
        });
    }

    {
        let mut group = c.benchmark_group("regular_detections");

        group.bench_function("little_tar", |b| {
            b.iter(|| test_detect(black_box("files/test.tar")))
        });

        group.bench_function("1mb_tar", |b| {
            b.iter(|| test_detect(black_box("files/test-0.tar")))
        });

        group.bench_function("multi", |b| {
            b.iter(|| {
                let _ = black_box(test_detect(black_box("files/hello.rar")));
                let _ = black_box(test_detect(black_box("files/hello.zip")));
                let _ = black_box(test_detect(black_box("files/rust-logo.png")));
            })
        });
    }

    {
        let mut group = c.benchmark_group("all-types");

        let mut files = list_files();
        for file in files {
            let name = file.file_stem().expect("name").to_str().expect("name");
            let ext = file
                .extension()
                .expect("extension")
                .to_str()
                .expect("extension");
            group.bench_with_input(format!("{name}-{ext}"), &file, |b, file| {
                b.iter(|| test_detect(file))
            });
        }
    }
}

criterion_group! {
    name = benches;
    config = Criterion::default().sample_size(50);
    targets = criterion_benchmark
}
criterion_main!(benches);