use crate::error::{Error, Result};
use std::path::{Component, Path, PathBuf};
pub fn sanitize_stream_key(key: &str) -> Result<String> {
if key.trim().is_empty() {
return Err(Error::Config("Stream key cannot be empty".into()));
}
let sanitized: String = key
.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => c,
'/' | '\\' | ':' | '\0' => '_',
_ => '_',
})
.collect();
if sanitized == ".." || sanitized == "." {
return Err(Error::Config(format!("Invalid stream key: {}", key)));
}
Ok(sanitized)
}
pub fn ensure_safe_path(root: &Path, child: &str) -> Result<PathBuf> {
let safe_child = sanitize_stream_key(child)?;
let path = root.join(safe_child);
if path.components().any(|c| matches!(c, Component::ParentDir)) {
return Err(Error::Config("Path traversal detected".into()));
}
Ok(path)
}
pub fn segment_filename(start_id: u64) -> String {
format!("{:020}.wal", start_id)
}
pub fn parse_segment_id(filename: &str) -> Option<u64> {
filename.strip_suffix(".wal")?.parse::<u64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitization() {
assert_eq!(sanitize_stream_key("valid-key").unwrap(), "valid-key");
assert_eq!(sanitize_stream_key("user/123").unwrap(), "user_123");
assert_eq!(sanitize_stream_key("../hack").unwrap(), ".._hack");
}
#[test]
fn test_filenames() {
let id = 12345;
let name = segment_filename(id);
assert_eq!(name, "00000000000000012345.wal");
assert_eq!(parse_segment_id(&name), Some(id));
}
}