use std::collections::HashSet;
use thiserror::Error;
use crate::LOCKFILE_FILENAME;
use crate::MANIFEST_FILENAME;
use crate::SIGNATURE_FILENAME;
use crate::relative_path::RelativePath;
#[derive(Debug, Error)]
pub enum TreeError {
#[error("reserved filename `{name}` is only permitted at the module root; found at `{path}`")]
ReservedFilename {
name: &'static str,
path: String,
},
#[error("paths collapse to the same NFC form `{nfc}`")]
AmbiguousPath {
nfc: String,
},
}
pub fn validate_tree<'a, I>(paths: I) -> Result<(), TreeError>
where
I: IntoIterator<Item = &'a RelativePath>,
{
let mut seen: HashSet<&RelativePath> = HashSet::new();
for path in paths {
if let Some((_, basename)) = path.as_str().rsplit_once('/')
&& let Some(reserved) = [MANIFEST_FILENAME, LOCKFILE_FILENAME, SIGNATURE_FILENAME]
.into_iter()
.find(|r| *r == basename)
{
return Err(TreeError::ReservedFilename {
name: reserved,
path: path.as_str().to_string(),
});
}
if !seen.insert(path) {
return Err(TreeError::AmbiguousPath {
nfc: path.as_str().to_string(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
fn rel(s: &str) -> RelativePath {
RelativePath::from_str(s).unwrap()
}
#[test]
fn accepts_root_reserved_filenames() {
validate_tree(&[
rel(MANIFEST_FILENAME),
rel(LOCKFILE_FILENAME),
rel(SIGNATURE_FILENAME),
rel("index.wdl"),
])
.unwrap();
}
#[test]
fn rejects_nested_manifest() {
let err = validate_tree(&[rel("src/module.json")]).unwrap_err();
assert!(matches!(
err,
TreeError::ReservedFilename {
name: MANIFEST_FILENAME,
..
}
));
}
#[test]
fn rejects_nested_lockfile() {
let err = validate_tree(&[rel("nested/dir/module-lock.json")]).unwrap_err();
assert!(matches!(
err,
TreeError::ReservedFilename {
name: LOCKFILE_FILENAME,
..
}
));
}
#[test]
fn rejects_nested_signature() {
let err = validate_tree(&[rel("sub/module.sig")]).unwrap_err();
assert!(matches!(
err,
TreeError::ReservedFilename {
name: SIGNATURE_FILENAME,
..
}
));
}
#[test]
fn rejects_paths_colliding_under_nfc() {
let err = validate_tree(&[rel("caf\u{00E9}.wdl"), rel("cafe\u{0301}.wdl")]).unwrap_err();
assert!(matches!(err, TreeError::AmbiguousPath { .. }));
}
#[test]
fn accepts_distinct_unicode_paths() {
validate_tree(&[rel("alpha.wdl"), rel("beta.wdl"), rel("caf\u{00E9}.wdl")]).unwrap();
}
}