use std::str::FromStr;
use crate::utils::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InputType {
Identifier,
TemplateName,
TemplateVar,
FilePath,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SanitizedInput {
inner: String,
input_type: InputType,
}
impl SanitizedInput {
pub fn new(input: impl Into<String>, input_type: InputType) -> Result<Self> {
let input = input.into();
let max_length = Self::max_length_for_type(input_type);
if input.len() > max_length {
return Err(Error::invalid_input(format!(
"Input too long: {} bytes (max {})",
input.len(),
max_length
)));
}
match input_type {
InputType::Identifier => Self::validate_identifier(&input)?,
InputType::TemplateName => Self::validate_template_name(&input)?,
InputType::TemplateVar => Self::validate_template_var(&input)?,
InputType::FilePath => Self::validate_file_path(&input)?,
}
Ok(Self {
inner: input,
input_type,
})
}
pub fn as_str(&self) -> &str {
&self.inner
}
pub fn into_string(self) -> String {
self.inner
}
pub fn input_type(&self) -> InputType {
self.input_type
}
fn max_length_for_type(input_type: InputType) -> usize {
match input_type {
InputType::Identifier => 256,
InputType::TemplateName => 512,
InputType::TemplateVar => 1024,
InputType::FilePath => 4096,
}
}
fn validate_identifier(input: &str) -> Result<()> {
if input.is_empty() {
return Err(Error::invalid_input("Identifier cannot be empty"));
}
for ch in input.chars() {
if !ch.is_alphanumeric() && ch != '_' && ch != '-' {
return Err(Error::invalid_input(format!(
"Invalid character in identifier: '{}'",
ch
)));
}
}
Ok(())
}
fn validate_template_name(input: &str) -> Result<()> {
if input.is_empty() {
return Err(Error::invalid_input("Template name cannot be empty"));
}
for ch in input.chars() {
if !ch.is_alphanumeric() && ch != '_' && ch != '-' && ch != '.' && ch != '/' {
return Err(Error::invalid_input(format!(
"Invalid character in template name: '{}'",
ch
)));
}
}
Ok(())
}
fn validate_template_var(input: &str) -> Result<()> {
const INJECTION_PATTERNS: &[&str] = &[
"{{",
"{%",
"${",
"<%",
"<script",
"javascript:",
"onerror=",
"onload=",
];
let lower = input.to_lowercase();
for pattern in INJECTION_PATTERNS {
if lower.contains(pattern) {
return Err(Error::invalid_input(format!(
"Template injection detected: '{}'",
pattern
)));
}
}
Ok(())
}
fn validate_file_path(input: &str) -> Result<()> {
if input.is_empty() {
return Err(Error::invalid_input("File path cannot be empty"));
}
if input.contains('\0') {
return Err(Error::invalid_input("File path contains null byte"));
}
Ok(())
}
}
impl AsRef<str> for SanitizedInput {
fn as_ref(&self) -> &str {
&self.inner
}
}
impl FromStr for SanitizedInput {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s, InputType::TemplateVar)
}
}
impl std::fmt::Display for SanitizedInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}