use super::{
GlobPattern, diagnostics,
errors::{GlobErrorContext, GlobErrorType, create_glob_error},
escape::escape_glob_literal_path,
};
use camino::{Utf8Path, Utf8PathBuf};
use minijinja::Error;
use std::{
sync::{Mutex, MutexGuard},
time::Instant,
};
pub(super) fn resolve_relative_glob_base(
base: &Utf8Path,
) -> std::result::Result<Utf8PathBuf, Error> {
resolve_relative_glob_base_for_template(base).map_err(super::GlobExpansionFailure::into_error)
}
fn resolve_relative_glob_base_for_template(
base: &Utf8Path,
) -> std::result::Result<Utf8PathBuf, super::GlobExpansionFailure> {
let canonical = dunce::canonicalize(base.as_std_path()).map_err(|error| {
super::GlobExpansionFailure::BaseCanonicalization(create_base_error(
base,
error.to_string(),
))
})?;
Utf8PathBuf::from_path_buf(canonical).map_err(|path| {
super::GlobExpansionFailure::Utf8Conversion(create_base_error(
base,
format!("canonical base path is not valid UTF-8: {}", path.display()),
))
})
}
fn create_base_error(base: &Utf8Path, detail: String) -> Error {
create_glob_error(
&GlobErrorContext {
pattern: base.to_string(),
error_char: char::from(0),
position: 0,
error_type: GlobErrorType::IoError,
},
Some(detail),
)
}
pub(in crate::manifest) struct GlobBaseCache {
base: Option<Utf8PathBuf>,
resolved: Mutex<Option<Utf8PathBuf>>,
}
impl GlobBaseCache {
pub(in crate::manifest) const fn new(base: Option<Utf8PathBuf>) -> Self {
Self {
base,
resolved: Mutex::new(None),
}
}
pub(super) fn mode(&self, pattern: &Utf8Path) -> &'static str {
if pattern.is_absolute() {
"absolute_pattern"
} else if self.base.is_some() {
"relative_with_base"
} else {
"relative_without_base"
}
}
fn resolve(&self) -> std::result::Result<Option<Utf8PathBuf>, super::GlobExpansionFailure> {
let _span = tracing::debug_span!("manifest.glob_base", operation = "resolve").entered();
let Some(base) = self.base.as_deref() else {
diagnostics::record_base_cache_bypass();
return Ok(None);
};
let cached = self
.lock_resolved(base)
.map_err(super::GlobExpansionFailure::BaseCanonicalization)?
.clone();
if cached.is_some() {
diagnostics::record_base_cache_hit();
return Ok(cached);
}
let started = Instant::now();
let canonical = match resolve_relative_glob_base_for_template(base) {
Ok(canonical) => {
diagnostics::record_base_cache_miss(started.elapsed());
canonical
}
Err(error) => {
diagnostics::record_base_cache_error(started.elapsed());
return Err(error);
}
};
let mut resolved = self
.lock_resolved(base)
.map_err(super::GlobExpansionFailure::BaseCanonicalization)?;
if let Some(published) = resolved.as_ref() {
return Ok(Some(published.clone()));
}
resolved.replace(canonical.clone());
Ok(Some(canonical))
}
fn lock_resolved(
&self,
base: &Utf8Path,
) -> std::result::Result<MutexGuard<'_, Option<Utf8PathBuf>>, Error> {
self.resolved.lock().map_err(|error| {
create_glob_error(
&GlobErrorContext {
pattern: base.to_string(),
error_char: char::from(0),
position: 0,
error_type: GlobErrorType::IoError,
},
Some(format!("manifest glob-base cache lock poisoned: {error}")),
)
})
}
}
pub(super) struct PreparedGlob {
pub(super) pattern: GlobPattern,
search: Option<String>,
pub(super) strip: Option<Utf8PathBuf>,
}
impl PreparedGlob {
pub(super) fn new(pattern: &str, base: Option<&Utf8Path>) -> std::result::Result<Self, Error> {
let pattern_state = GlobPattern::new(pattern)?;
let normalized = pattern_state.normalized();
let resolved_base = match base {
Some(dir) if !Utf8Path::new(normalized).is_absolute() => {
Some(resolve_relative_glob_base(dir)?)
}
_ => None,
};
Ok(Self::from_pattern_and_base(pattern_state, resolved_base))
}
#[cfg(test)]
pub(super) fn new_with_base_cache(
pattern: &str,
base: &GlobBaseCache,
) -> std::result::Result<Self, Error> {
Self::new_with_base_cache_for_template(pattern, base)
.map_err(super::GlobExpansionFailure::into_error)
}
pub(super) fn new_with_base_cache_for_template(
pattern: &str,
base: &GlobBaseCache,
) -> std::result::Result<Self, super::GlobExpansionFailure> {
let pattern_state =
GlobPattern::new(pattern).map_err(super::GlobExpansionFailure::InvalidPattern)?;
let resolved_base = (!Utf8Path::new(pattern_state.normalized()).is_absolute())
.then(|| base.resolve())
.transpose()?
.flatten();
Ok(Self::from_pattern_and_base(pattern_state, resolved_base))
}
fn from_pattern_and_base(pattern: GlobPattern, base: Option<Utf8PathBuf>) -> Self {
let (search, strip) = base.map_or_else(
|| (None, None),
|dir| {
let escaped = escape_glob_literal_path(&dir);
let separator = std::path::MAIN_SEPARATOR;
(
Some(format!("{escaped}{separator}{}", pattern.normalized())),
Some(dir),
)
},
);
Self {
pattern,
search,
strip,
}
}
pub(super) fn search(&self) -> &str {
self.search
.as_deref()
.unwrap_or_else(|| self.pattern.normalized())
}
}