use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
mod cache;
mod codec;
mod paths;
mod probe;
pub use cache::{Cache, CacheEntry};
pub use codec::{
codec_id_for, last_codec_allocator_negotiation, lookup_record, make_decoder,
output_pixel_format, register_factory_for_id, CodecAllocatorNegotiation, DiscoveryRecord,
};
pub use paths::{cache_file_path, discovery_paths};
pub use probe::Kind;
const ALLOW_MISSING_DIR: bool = true;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveryEntry {
pub path: PathBuf,
pub mtime_unix: i64,
pub size_bytes: u64,
pub kind: Kind,
pub fourccs: Vec<String>,
pub clsid: Option<String>,
}
impl DiscoveryEntry {
pub fn matches(&self, path: &Path, mtime: i64, size: u64) -> bool {
self.path == path && self.mtime_unix == mtime && self.size_bytes == size
}
}
pub fn discover(paths: &[PathBuf]) -> Vec<DiscoveryEntry> {
let cache_path = cache_file_path();
let mut cache = Cache::load(&cache_path).unwrap_or_default();
let mut out: Vec<DiscoveryEntry> = Vec::new();
for dir in paths {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => {
if !ALLOW_MISSING_DIR {
log::warn!("vfw discovery: cannot read {:?}", dir);
}
continue;
}
};
for entry in entries.flatten() {
let path = entry.path();
if !is_codec_candidate(&path) {
continue;
}
let (mtime, size) = match file_meta(&path) {
Some(m) => m,
None => continue,
};
if let Some(cached) = cache.lookup(&path, mtime, size) {
out.push(cached.clone());
continue;
}
let bytes = match fs::read(&path) {
Ok(b) => b,
Err(e) => {
log::debug!("vfw discovery: read {:?} failed: {e}", path);
continue;
}
};
let probed = probe::probe_bytes(&bytes);
let entry = DiscoveryEntry {
path: path.clone(),
mtime_unix: mtime,
size_bytes: size,
kind: probed.kind,
fourccs: probed.fourccs,
clsid: probed.clsid,
};
cache.upsert(entry.clone());
out.push(entry);
}
}
let _ = cache.save_atomic(&cache_path);
out
}
fn file_meta(path: &Path) -> Option<(i64, u64)> {
let md = fs::metadata(path).ok()?;
let size = md.len();
let mtime = md.modified().ok().and_then(systime_to_unix).unwrap_or(0);
Some((mtime, size))
}
fn systime_to_unix(t: SystemTime) -> Option<i64> {
match t.duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => Some(d.as_secs() as i64),
Err(e) => Some(-(e.duration().as_secs() as i64)),
}
}
fn is_codec_candidate(path: &Path) -> bool {
if !path.is_file() {
return false;
}
match path.extension().and_then(|s| s.to_str()) {
Some(ext) => {
let lower = ext.to_ascii_lowercase();
lower == "dll" || lower == "ax"
}
None => false,
}
}
pub fn discover_and_register(ctx: &mut oxideav_core::RuntimeContext) -> usize {
let paths = discovery_paths();
let entries = discover(&paths);
let mut registered = 0usize;
let mut fourccs_seen: Vec<String> = Vec::new();
for entry in entries {
if matches!(entry.kind, Kind::Unsupported) {
log::debug!("vfw discovery: {:?} is unsupported", entry.path);
continue;
}
for fcc in &entry.fourccs {
let codec_id_str = codec::codec_id_for(&entry.path, fcc);
codec::register_factory_for_id(
&codec_id_str,
DiscoveryRecord {
dll_path: entry.path.clone(),
fourcc: fcc.clone(),
kind: entry.kind,
clsid: entry.clsid.clone(),
},
);
codec::register_codec_info(ctx, &codec_id_str, fcc);
fourccs_seen.push(fcc.clone());
registered += 1;
}
}
log::debug!(
"vfw: discovered {} codecs from {:?}: {:?}",
registered,
paths,
fourccs_seen
);
registered
}
#[cfg(test)]
pub(crate) mod test_tmpdir {
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct Tmp(pub PathBuf);
impl Tmp {
pub fn new(label: &str) -> Self {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path =
std::env::temp_dir().join(format!("oxideav-vfw-disc-{label}-{pid}-{nanos}-{n}"));
std::fs::create_dir_all(&path).unwrap();
Tmp(path)
}
pub fn path(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for Tmp {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
}
#[cfg(test)]
mod tests {
use super::test_tmpdir::Tmp;
use super::*;
use std::io::Write;
#[test]
fn discovery_on_nonexistent_path_returns_empty_cleanly() {
let entries = discover(&[PathBuf::from("/this/does/not/exist/anywhere")]);
assert!(entries.is_empty());
}
#[test]
fn discovery_on_empty_dir_returns_empty() {
let tmp = Tmp::new("empty");
let entries = discover(&[tmp.path().to_path_buf()]);
assert!(entries.is_empty());
}
#[test]
fn discovery_skips_non_pe_files() {
let tmp = Tmp::new("nonpe");
let bogus = tmp.path().join("garbage.dll");
let mut f = fs::File::create(&bogus).unwrap();
f.write_all(b"this is not a PE32 file").unwrap();
let entries = discover(&[tmp.path().to_path_buf()]);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].kind, Kind::Unsupported);
assert!(entries[0].fourccs.is_empty());
}
#[test]
fn is_codec_candidate_matches_dll_and_ax() {
let tmp = Tmp::new("ext");
let dll = tmp.path().join("foo.DLL");
let ax = tmp.path().join("bar.Ax");
let txt = tmp.path().join("baz.txt");
for p in [&dll, &ax, &txt] {
fs::File::create(p).unwrap();
}
assert!(is_codec_candidate(&dll));
assert!(is_codec_candidate(&ax));
assert!(!is_codec_candidate(&txt));
}
}