use std::path::{Component, Path};
use crate::core::limits::{
within_range, MAX_KEY_LEN, MAX_KEY_SEGMENT_LEN, MAX_NAMESPACE_LEN, MAX_PROJECT_NAME_LEN,
MAX_SEGMENT_COUNT, MAX_STORE_PATH_LEN, MAX_VALUE_LEN, MIN_KEY_LEN, MIN_KEY_SEGMENT_LEN,
MIN_NAMESPACE_LEN, MIN_PROJECT_NAME_LEN, MIN_VALUE_LEN,
};
use crate::error::{Result, ValidationError};
pub fn validate_key(input: &str) -> Result<()> {
validate_common_text("key", input, MIN_KEY_LEN, MAX_KEY_LEN)?;
if input.starts_with('/') || input.ends_with('/') {
return Err(
ValidationError::invalid_format("key", "must not start or end with '/'").into(),
);
}
if input.contains("//") {
return Err(
ValidationError::invalid_format("key", "must not contain empty path segments").into(),
);
}
let segments: Vec<&str> = input.split('/').collect();
if segments.len() > MAX_SEGMENT_COUNT {
return Err(
ValidationError::too_long("key_segments", segments.len(), MAX_SEGMENT_COUNT).into(),
);
}
for segment in segments {
validate_segment("key", segment)?;
}
Ok(())
}
pub fn validate_namespace(input: &str) -> Result<()> {
validate_common_text("namespace", input, MIN_NAMESPACE_LEN, MAX_NAMESPACE_LEN)?;
if input.starts_with('/') || input.ends_with('/') {
return Err(
ValidationError::invalid_format("namespace", "must not start or end with '/'").into(),
);
}
if input.contains("//") {
return Err(ValidationError::invalid_format(
"namespace",
"must not contain empty path segments",
)
.into());
}
let segments: Vec<&str> = input.split('/').collect();
if segments.len() > MAX_SEGMENT_COUNT {
return Err(ValidationError::too_long(
"namespace_segments",
segments.len(),
MAX_SEGMENT_COUNT,
)
.into());
}
for segment in segments {
validate_segment("namespace", segment)?;
}
Ok(())
}
pub fn validate_key_leaf(input: &str) -> Result<()> {
validate_segment("key_leaf", input)
}
pub fn validate_value(input: &str) -> Result<()> {
validate_common_text("value", input, MIN_VALUE_LEN, MAX_VALUE_LEN)?;
if input.contains('\0') {
return Err(ValidationError::invalid_format("value", "must not contain NUL bytes").into());
}
Ok(())
}
pub fn validate_project_name(input: &str) -> Result<()> {
validate_common_text(
"project_name",
input,
MIN_PROJECT_NAME_LEN,
MAX_PROJECT_NAME_LEN,
)?;
for (index, ch) in input.char_indices() {
if is_project_name_char(ch) {
continue;
}
return Err(ValidationError::InvalidCharacter {
field: "project_name",
character: ch,
index,
}
.into());
}
if input.starts_with('-') || input.ends_with('-') {
return Err(ValidationError::invalid_format(
"project_name",
"must not start or end with '-'",
)
.into());
}
Ok(())
}
pub fn validate_store_path(path: &Path) -> Result<()> {
let rendered = path.to_string_lossy();
if rendered.is_empty() {
return Err(ValidationError::empty("store_path").into());
}
if rendered.len() > MAX_STORE_PATH_LEN {
return Err(
ValidationError::too_long("store_path", rendered.len(), MAX_STORE_PATH_LEN).into(),
);
}
let file_name = path.file_name().ok_or_else(|| {
ValidationError::invalid_path("store_path", "path must include a file name")
})?;
if file_name.to_string_lossy().trim().is_empty() {
return Err(
ValidationError::invalid_path("store_path", "file name must not be empty").into(),
);
}
for component in path.components() {
validate_path_component(component)?;
}
Ok(())
}
fn validate_segment(field: &'static str, segment: &str) -> Result<()> {
validate_common_text(field, segment, MIN_KEY_SEGMENT_LEN, MAX_KEY_SEGMENT_LEN)?;
if segment == "." || segment == ".." {
return Err(
ValidationError::invalid_segment(field, "reserved segment is not allowed").into(),
);
}
for (index, ch) in segment.char_indices() {
if is_segment_char(ch) {
continue;
}
return Err(ValidationError::InvalidCharacter {
field,
character: ch,
index,
}
.into());
}
Ok(())
}
fn validate_common_text(
field: &'static str,
input: &str,
min_len: usize,
max_len: usize,
) -> Result<()> {
let len = input.len();
if len == 0 && min_len > 0 {
return Err(ValidationError::empty(field).into());
}
if !within_range(len, min_len, max_len) {
if len < min_len {
return Err(ValidationError::too_short(field, len, min_len).into());
}
return Err(ValidationError::too_long(field, len, max_len).into());
}
if input.contains('\0') {
return Err(ValidationError::invalid_format(field, "must not contain NUL bytes").into());
}
Ok(())
}
fn validate_path_component(component: Component<'_>) -> Result<()> {
match component {
Component::ParentDir => Err(ValidationError::invalid_path(
"store_path",
"parent traversal ('..') is not allowed",
)
.into()),
Component::Normal(name) => {
let rendered = name.to_string_lossy();
if rendered.trim().is_empty() {
return Err(ValidationError::invalid_path(
"store_path",
"path segment must not be empty",
)
.into());
}
if rendered == "." || rendered == ".." {
return Err(ValidationError::invalid_path(
"store_path",
"reserved path segment is not allowed",
)
.into());
}
Ok(())
}
Component::CurDir | Component::RootDir | Component::Prefix(_) => Ok(()),
}
}
#[must_use]
const fn is_segment_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')
}
#[must_use]
const fn is_project_name_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')
}