use std::path::PathBuf;
use thiserror::Error;
use super::{ManifestData, parse_manifest};
use crate::error::LoaderError;
#[derive(Debug, Error)]
pub enum ScanDiagnostic {
#[error("failed to read directory `{path}`: {source}")]
DirectoryReadFailed {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to access entry in `{dir}`: {source}")]
EntryReadFailed {
dir: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse bundle at `{path}`: {source}")]
ManifestParseFailed {
path: PathBuf,
#[source]
source: LoaderError,
},
}
#[derive(Debug)]
pub struct ScanResult {
pub found: Vec<(PathBuf, ManifestData)>,
pub diagnostics: Vec<ScanDiagnostic>,
}
pub fn scan_dirs(dirs: &[PathBuf]) -> ScanResult {
let mut found: Vec<(PathBuf, ManifestData)> = Vec::new();
let mut diagnostics: Vec<ScanDiagnostic> = Vec::new();
for dir in dirs {
if !dir.is_dir() {
continue;
}
let read_dir: std::fs::ReadDir = match std::fs::read_dir(dir) {
Ok(read_dir) => read_dir,
Err(source) => {
diagnostics.push(ScanDiagnostic::DirectoryReadFailed {
path: dir.clone(),
source,
});
continue;
}
};
for entry in read_dir {
let entry: std::fs::DirEntry = match entry {
Ok(entry) => entry,
Err(source) => {
diagnostics.push(ScanDiagnostic::EntryReadFailed {
dir: dir.clone(),
source,
});
continue;
}
};
let path: PathBuf = entry.path();
if !path.is_dir() {
continue;
}
let manifest_path: PathBuf = path.join("manifest.toml");
if !manifest_path.exists() {
continue;
}
match parse_manifest(&path) {
Ok(manifest) => found.push((path, manifest)),
Err(source) => {
diagnostics.push(ScanDiagnostic::ManifestParseFailed { path, source });
}
}
}
}
ScanResult { found, diagnostics }
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::{ScanDiagnostic, ScanResult, scan_dirs};
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tempfile::TempDir;
fn write_valid_bundle(root: &std::path::Path, name: &str) {
let bundle_dir: PathBuf = root.join(name);
fs::create_dir_all(&bundle_dir).expect("create bundle dir");
let manifest: String = format!(
"id = 1\nname = \"{name}\"\nloader = \"native\"\nfile = \"{name}.so\"\nversion = \"1.0\"\n"
);
fs::write(bundle_dir.join("manifest.toml"), manifest).expect("write manifest");
}
#[test]
fn valid_bundle_returned_corrupt_and_unreadable_reported() {
let tmp: TempDir = TempDir::new().expect("tmp dir");
write_valid_bundle(tmp.path(), "good_bundle");
let bad_dir: PathBuf = tmp.path().join("bad_bundle");
fs::create_dir_all(&bad_dir).expect("create bad bundle dir");
fs::write(bad_dir.join("manifest.toml"), b"NOT VALID TOML ===== [[[")
.expect("write bad manifest");
let dirs: &[PathBuf] = &[tmp.path().to_path_buf()];
let result: ScanResult = scan_dirs(dirs);
assert_eq!(result.found.len(), 1, "expected exactly the valid bundle");
assert_eq!(result.found[0].1.name, "good_bundle");
let parse_diags: Vec<&ScanDiagnostic> = result
.diagnostics
.iter()
.filter(|d| matches!(d, ScanDiagnostic::ManifestParseFailed { .. }))
.collect();
assert_eq!(parse_diags.len(), 1, "expected one parse diagnostic");
let msg: String = parse_diags[0].to_string();
assert!(
msg.contains("bad_bundle"),
"diagnostic must name the bundle: {msg}"
);
}
#[test]
fn missing_directory_skipped_without_diagnostic() {
let tmp: TempDir = TempDir::new().expect("tmp dir");
let missing: PathBuf = tmp.path().join("does_not_exist");
let result: ScanResult = scan_dirs(&[missing]);
assert!(result.found.is_empty());
assert!(result.diagnostics.is_empty());
}
#[cfg(unix)]
#[test]
fn unreadable_directory_reported_as_diagnostic() {
let tmp: TempDir = TempDir::new().expect("tmp dir");
let locked: PathBuf = tmp.path().join("locked");
fs::create_dir_all(&locked).expect("create locked dir");
let mut perms: fs::Permissions = fs::metadata(&locked).expect("metadata").permissions();
perms.set_mode(0o000);
fs::set_permissions(&locked, perms).expect("chmod 000");
let result: ScanResult = scan_dirs(core::slice::from_ref(&locked));
let mut restore: fs::Permissions = fs::Permissions::from_mode(0o755);
restore.set_mode(0o755);
let _ = fs::set_permissions(&locked, restore);
if !result.diagnostics.is_empty() {
assert!(
result
.diagnostics
.iter()
.any(|d| matches!(d, ScanDiagnostic::DirectoryReadFailed { .. })),
"expected a DirectoryReadFailed diagnostic"
);
}
}
#[test]
fn empty_dir_yields_no_bundles_no_diagnostics() {
let tmp: TempDir = TempDir::new().expect("tmp dir");
let result: ScanResult = scan_dirs(&[tmp.path().to_path_buf()]);
assert!(result.found.is_empty());
assert!(result.diagnostics.is_empty());
}
}