use std::path::{Component, Path, PathBuf};
use super::{path_string, AsrError, AsrResult};
pub(crate) fn normalize_requested_path(requested: &str) -> AsrResult<String> {
let trimmed = requested.trim();
if trimmed.is_empty() {
return Err(AsrError::new("invalid_path", "Path must not be empty"));
}
if trimmed.contains('\0') {
return Err(AsrError::with_path(
"invalid_path",
"Path must not contain NUL bytes",
trimmed,
));
}
let normalized_input = trimmed.replace('\\', "/");
let path = Path::new(&normalized_input);
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::Normal(part) => {
if part.to_string_lossy() == ".git" {
return Err(AsrError::with_path(
"invalid_path",
"ASR file commands do not expose Git internals",
trimmed,
));
}
normalized.push(part)
}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(AsrError::with_path(
"invalid_path",
"Path must be relative and must not contain parent directory components",
trimmed,
));
}
}
}
if normalized.as_os_str().is_empty() {
return Err(AsrError::new("invalid_path", "Path must not be empty"));
}
Ok(path_string(&normalized))
}
pub(crate) fn validate_top_k(top_k: usize, max_top_k: usize) -> AsrResult<()> {
if top_k == 0 || top_k > max_top_k {
return Err(AsrError::new(
"invalid_top_k",
format!("top-k must be between 1 and {max_top_k}"),
));
}
Ok(())
}
pub(crate) fn validate_context_budget(budget: usize, max_context_budget: usize) -> AsrResult<()> {
if budget == 0 || budget > max_context_budget {
return Err(AsrError::new(
"invalid_budget",
format!("budget must be between 1 and {max_context_budget}"),
));
}
Ok(())
}
pub(crate) fn validate_repo_name(name: &str) -> AsrResult<()> {
if name.is_empty()
|| name.starts_with('.')
|| name.contains('/')
|| name.contains('\\')
|| !name
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
{
return Err(AsrError::new(
"invalid_repo_name",
"Repository name must use ASCII letters, numbers, '.', '-', or '_'",
));
}
Ok(())
}
pub(crate) fn validate_git_ref(value: &str, field: &'static str) -> AsrResult<()> {
let trimmed = value.trim();
let invalid = trimmed.is_empty()
|| trimmed != value
|| trimmed.len() > 256
|| trimmed.starts_with('-')
|| trimmed.contains('\0')
|| trimmed.contains('\\')
|| trimmed.contains(':')
|| trimmed.contains("..")
|| trimmed.contains("@{")
|| trimmed == "@"
|| trimmed.contains("--")
|| trimmed
.chars()
.any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace());
if invalid {
return Err(AsrError::new(
"invalid_git_ref",
format!(
"{field} Git ref must be a bounded commit-ish ref without whitespace, pathspecs, option prefixes, reflog selectors, or range syntax"
),
));
}
Ok(())
}