use crate::{
openapi::{OpenApiOperation, OpenApiSecurityRequirement, OpenApiSpec},
Error, Result,
};
use base32::Alphabet;
use base64::{engine::general_purpose, Engine as _};
use jsonschema::{self, Draft, Validator as JSONSchema};
use prost_reflect::{DescriptorPool, DynamicMessage};
use serde_json::{json, Value};
#[derive(Debug)]
pub enum Validator {
JsonSchema(JSONSchema),
OpenApi31Schema(JSONSchema, Value),
OpenApi(Box<OpenApiSpec>),
Protobuf(DescriptorPool),
}
impl Validator {
pub fn from_json_schema(schema: &Value) -> Result<Self> {
let compiled = jsonschema::options()
.with_draft(Draft::Draft7)
.build(schema)
.map_err(|e| Error::validation(format!("Failed to compile JSON schema: {}", e)))?;
Ok(Self::JsonSchema(compiled))
}
pub fn from_openapi31_schema(schema: &Value) -> Result<Self> {
let compiled =
jsonschema::options().with_draft(Draft::Draft7).build(schema).map_err(|e| {
Error::validation(format!("Failed to compile OpenAPI 3.1 schema: {}", e))
})?;
Ok(Self::OpenApi31Schema(compiled, schema.clone()))
}
pub fn from_openapi(spec: &Value) -> Result<Self> {
if let Some(openapi_version) = spec.get("openapi") {
if let Some(version_str) = openapi_version.as_str() {
if !version_str.starts_with("3.") {
return Err(Error::validation(format!(
"Unsupported OpenAPI version: {}. Only 3.x is supported",
version_str
)));
}
}
}
let openapi_spec = OpenApiSpec::from_json(spec.clone())
.map_err(|e| Error::validation(format!("Failed to parse OpenAPI spec: {}", e)))?;
Ok(Self::OpenApi(Box::new(openapi_spec)))
}
pub fn from_protobuf(descriptor: &[u8]) -> Result<Self> {
let mut pool = DescriptorPool::new();
pool.decode_file_descriptor_set(descriptor)
.map_err(|e| Error::validation(format!("Invalid protobuf descriptor: {}", e)))?;
Ok(Self::Protobuf(pool))
}
pub fn validate(&self, data: &Value) -> Result<()> {
match self {
Self::JsonSchema(schema) => {
let mut errors = Vec::new();
for error in schema.iter_errors(data) {
errors.push(error.to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(Error::validation(format!("Validation failed: {}", errors.join(", "))))
}
}
Self::OpenApi31Schema(schema, original_schema) => {
let mut errors = Vec::new();
for error in schema.iter_errors(data) {
errors.push(error.to_string());
}
if !errors.is_empty() {
return Err(Error::validation(format!(
"Validation failed: {}",
errors.join(", ")
)));
}
self.validate_openapi31_schema(data, original_schema)
}
Self::OpenApi(_spec) => {
if data.is_object() {
Ok(())
} else {
Err(Error::validation("OpenAPI validation expects an object".to_string()))
}
}
Self::Protobuf(_) => {
Err(Error::validation(
"Protobuf validation requires binary data — use validate_protobuf() functions directly".to_string()
))
}
}
}
pub fn is_implemented(&self) -> bool {
match self {
Self::JsonSchema(_) => true,
Self::OpenApi31Schema(_, _) => true,
Self::OpenApi(_) => true, Self::Protobuf(_) => false, }
}
pub fn validate_openapi_ext(&self, data: &Value, openapi_schema: &Value) -> Result<()> {
match self {
Self::JsonSchema(_) => {
self.validate_openapi31_schema(data, openapi_schema)
}
Self::OpenApi31Schema(_, _) => {
self.validate_openapi31_schema(data, openapi_schema)
}
Self::OpenApi(_spec) => {
if data.is_object() {
Ok(())
} else {
Err(Error::validation("OpenAPI validation expects an object".to_string()))
}
}
Self::Protobuf(_) => {
Err(Error::validation(
"Protobuf validation requires binary data — use validate_protobuf() functions directly".to_string()
))
}
}
}
fn validate_openapi31_schema(&self, data: &Value, schema: &Value) -> Result<()> {
self.validate_openapi31_constraints(data, schema, "")
}
fn validate_openapi31_constraints(
&self,
data: &Value,
schema: &Value,
path: &str,
) -> Result<()> {
let schema_obj = schema
.as_object()
.ok_or_else(|| Error::validation(format!("{}: Schema must be an object", path)))?;
if let Some(type_str) = schema_obj.get("type").and_then(|v| v.as_str()) {
match type_str {
"number" | "integer" => self.validate_number_constraints(data, schema_obj, path)?,
"array" => self.validate_array_constraints(data, schema_obj, path)?,
"object" => self.validate_object_constraints(data, schema_obj, path)?,
"string" => self.validate_string_constraints(data, schema_obj, path)?,
_ => {} }
}
if let Some(all_of) = schema_obj.get("allOf").and_then(|v| v.as_array()) {
for subschema in all_of {
self.validate_openapi31_constraints(data, subschema, path)?;
}
}
if let Some(any_of) = schema_obj.get("anyOf").and_then(|v| v.as_array()) {
let mut errors = Vec::new();
for subschema in any_of {
if let Err(e) = self.validate_openapi31_constraints(data, subschema, path) {
errors.push(e.to_string());
} else {
return Ok(());
}
}
if !errors.is_empty() {
return Err(Error::validation(format!(
"{}: No subschema in anyOf matched: {}",
path,
errors.join(", ")
)));
}
}
if let Some(one_of) = schema_obj.get("oneOf").and_then(|v| v.as_array()) {
let mut matches = 0;
for subschema in one_of {
if self.validate_openapi31_constraints(data, subschema, path).is_ok() {
matches += 1;
}
}
if matches != 1 {
return Err(Error::validation(format!(
"{}: Expected exactly one subschema in oneOf to match, got {}",
path, matches
)));
}
}
if let Some(content_encoding) = schema_obj.get("contentEncoding").and_then(|v| v.as_str()) {
self.validate_content_encoding(data.as_str(), content_encoding, path)?;
}
Ok(())
}
fn validate_number_constraints(
&self,
data: &Value,
schema: &serde_json::Map<String, Value>,
path: &str,
) -> Result<()> {
let num = data
.as_f64()
.ok_or_else(|| Error::validation(format!("{}: Expected number, got {}", path, data)))?;
if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
if multiple_of > 0.0 && (num / multiple_of) % 1.0 != 0.0 {
return Err(Error::validation(format!(
"{}: {} is not a multiple of {}",
path, num, multiple_of
)));
}
}
if let Some(excl_min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
if num <= excl_min {
return Err(Error::validation(format!(
"{}: {} must be greater than {}",
path, num, excl_min
)));
}
}
if let Some(excl_max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
if num >= excl_max {
return Err(Error::validation(format!(
"{}: {} must be less than {}",
path, num, excl_max
)));
}
}
Ok(())
}
fn validate_array_constraints(
&self,
data: &Value,
schema: &serde_json::Map<String, Value>,
path: &str,
) -> Result<()> {
let arr = data
.as_array()
.ok_or_else(|| Error::validation(format!("{}: Expected array, got {}", path, data)))?;
if let Some(min_items) = schema.get("minItems").and_then(|v| v.as_u64()).map(|v| v as usize)
{
if arr.len() < min_items {
return Err(Error::validation(format!(
"{}: Array has {} items, minimum is {}",
path,
arr.len(),
min_items
)));
}
}
if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()).map(|v| v as usize)
{
if arr.len() > max_items {
return Err(Error::validation(format!(
"{}: Array has {} items, maximum is {}",
path,
arr.len(),
max_items
)));
}
}
if let Some(unique) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
if unique && !self.has_unique_items(arr) {
return Err(Error::validation(format!("{}: Array items must be unique", path)));
}
}
if let Some(items_schema) = schema.get("items") {
for (idx, item) in arr.iter().enumerate() {
let item_path = format!("{}[{}]", path, idx);
self.validate_openapi31_constraints(item, items_schema, &item_path)?;
}
}
Ok(())
}
fn validate_object_constraints(
&self,
data: &Value,
schema: &serde_json::Map<String, Value>,
path: &str,
) -> Result<()> {
let obj = data
.as_object()
.ok_or_else(|| Error::validation(format!("{}: Expected object, got {}", path, data)))?;
if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
for req_prop in required {
if let Some(prop_name) = req_prop.as_str() {
if !obj.contains_key(prop_name) {
return Err(Error::validation(format!(
"{}: Missing required property '{}'",
path, prop_name
)));
}
}
}
}
if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
for (prop_name, prop_schema) in properties {
if let Some(prop_value) = obj.get(prop_name) {
let prop_path = format!("{}/{}", path, prop_name);
self.validate_openapi31_constraints(prop_value, prop_schema, &prop_path)?;
}
}
}
Ok(())
}
fn validate_string_constraints(
&self,
data: &Value,
schema: &serde_json::Map<String, Value>,
path: &str,
) -> Result<()> {
let _str_val = data
.as_str()
.ok_or_else(|| Error::validation(format!("{}: Expected string, got {}", path, data)))?;
if schema.get("contentEncoding").is_some() {
}
Ok(())
}
fn validate_content_encoding(
&self,
data: Option<&str>,
encoding: &str,
path: &str,
) -> Result<()> {
let str_data = data.ok_or_else(|| {
Error::validation(format!("{}: Content encoding requires string data", path))
})?;
match encoding {
"base64" => {
if general_purpose::STANDARD.decode(str_data).is_err() {
return Err(Error::validation(format!("{}: Invalid base64 encoding", path)));
}
}
"base64url" => {
use base64::engine::general_purpose::URL_SAFE;
use base64::Engine;
if URL_SAFE.decode(str_data).is_err() {
return Err(Error::validation(format!("{}: Invalid base64url encoding", path)));
}
}
"base32" => {
if base32::decode(Alphabet::Rfc4648 { padding: false }, str_data).is_none() {
return Err(Error::validation(format!("{}: Invalid base32 encoding", path)));
}
}
"hex" | "binary" => {
if hex::decode(str_data).is_err() {
return Err(Error::validation(format!(
"{}: Invalid {} encoding",
path, encoding
)));
}
}
_ => {
tracing::warn!(
"{}: Unknown content encoding '{}', skipping validation",
path,
encoding
);
}
}
Ok(())
}
fn has_unique_items(&self, arr: &[Value]) -> bool {
let mut seen = std::collections::HashSet::new();
for item in arr {
let item_str = serde_json::to_string(item).unwrap_or_default();
if !seen.insert(item_str) {
return false;
}
}
true
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn failure(errors: Vec<String>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
}
}
pub fn with_warning(mut self, warning: String) -> Self {
self.warnings.push(warning);
self
}
}
pub fn validate_json_schema(data: &Value, schema: &Value) -> ValidationResult {
match Validator::from_json_schema(schema) {
Ok(validator) => match validator.validate(data) {
Ok(_) => ValidationResult::success(),
Err(Error::Validation { message }) => ValidationResult::failure(vec![message]),
Err(e) => ValidationResult::failure(vec![format!("Unexpected error: {}", e)]),
},
Err(e) => ValidationResult::failure(vec![format!("Schema compilation error: {}", e)]),
}
}
pub fn validate_openapi(data: &Value, spec: &Value) -> ValidationResult {
let spec_obj = match spec.as_object() {
Some(obj) => obj,
None => {
return ValidationResult::failure(vec!["OpenAPI spec must be an object".to_string()])
}
};
let mut errors = Vec::new();
if !spec_obj.contains_key("openapi") {
errors.push("Missing required 'openapi' field".to_string());
} else if let Some(version) = spec_obj.get("openapi").and_then(|v| v.as_str()) {
if !version.starts_with("3.") {
errors.push(format!("Unsupported OpenAPI version: {}. Only 3.x is supported", version));
}
}
if !spec_obj.contains_key("info") {
errors.push("Missing required 'info' field".to_string());
} else if let Some(info) = spec_obj.get("info").and_then(|v| v.as_object()) {
if !info.contains_key("title") {
errors.push("Missing required 'info.title' field".to_string());
}
if !info.contains_key("version") {
errors.push("Missing required 'info.version' field".to_string());
}
}
if !spec_obj.contains_key("paths") {
errors.push("Missing required 'paths' field".to_string());
}
if !errors.is_empty() {
return ValidationResult::failure(errors);
}
if serde_json::from_value::<openapiv3::OpenAPI>(spec.clone()).is_ok() {
let _spec_wrapper = OpenApiSpec::from_json(spec.clone()).unwrap_or_else(|_| {
OpenApiSpec::from_json(json!({}))
.expect("Empty JSON object should always create valid OpenApiSpec")
});
if data.is_object() {
ValidationResult::success()
.with_warning("OpenAPI schema validation available - use validate_openapi_with_path for operation-specific validation".to_string())
} else {
ValidationResult::failure(vec![
"Request/response data must be a JSON object".to_string()
])
}
} else {
ValidationResult::failure(vec!["Failed to parse OpenAPI specification".to_string()])
}
}
pub fn validate_openapi_operation(
_data: &Value,
spec: &OpenApiSpec,
path: &str,
method: &str,
_is_request: bool,
) -> ValidationResult {
let mut errors = Vec::new();
if let Some(path_item_ref) = spec.spec.paths.paths.get(path) {
if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
let operation = match method.to_uppercase().as_str() {
"GET" => path_item.get.as_ref(),
"POST" => path_item.post.as_ref(),
"PUT" => path_item.put.as_ref(),
"DELETE" => path_item.delete.as_ref(),
"PATCH" => path_item.patch.as_ref(),
"HEAD" => path_item.head.as_ref(),
"OPTIONS" => path_item.options.as_ref(),
_ => None,
};
if operation.is_some() {
} else {
errors.push(format!("Method {} not found for path {}", method, path));
}
} else {
errors
.push(format!("Path {} contains a reference, not supported for validation", path));
}
} else {
errors.push(format!("Path {} not found in OpenAPI spec", path));
}
if errors.is_empty() {
ValidationResult::success()
} else {
ValidationResult::failure(errors)
}
}
pub fn validate_protobuf(data: &[u8], descriptor_data: &[u8]) -> ValidationResult {
let mut pool = DescriptorPool::new();
if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
}
let Some(message_descriptor) = pool.all_messages().next() else {
return ValidationResult::failure(vec![
"Protobuf descriptor set does not contain any message descriptors".to_string(),
]);
};
match DynamicMessage::decode(message_descriptor, data) {
Ok(_) => ValidationResult::success(),
Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
}
}
pub fn validate_protobuf_message(
data: &[u8],
message_descriptor: &prost_reflect::MessageDescriptor,
) -> Result<()> {
match DynamicMessage::decode(message_descriptor.clone(), data) {
Ok(_) => Ok(()),
Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
}
}
pub fn validate_protobuf_with_type(
data: &[u8],
descriptor_data: &[u8],
message_type_name: &str,
) -> ValidationResult {
let mut pool = DescriptorPool::new();
if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
}
let descriptor = pool.get_message_by_name(message_type_name).or_else(|| {
pool.all_messages().find(|msg| {
msg.name() == message_type_name || msg.full_name().ends_with(message_type_name)
})
});
let Some(message_descriptor) = descriptor else {
return ValidationResult::failure(vec![format!(
"Message type '{}' not found in descriptor set",
message_type_name
)]);
};
match DynamicMessage::decode(message_descriptor, data) {
Ok(_) => ValidationResult::success(),
Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
}
}
pub fn validate_openapi_security(
spec: &OpenApiSpec,
security_requirements: &[OpenApiSecurityRequirement],
auth_header: Option<&str>,
api_key: Option<&str>,
) -> ValidationResult {
match spec.validate_security_requirements(security_requirements, auth_header, api_key) {
Ok(_) => ValidationResult::success(),
Err(e) => ValidationResult::failure(vec![format!("Security validation failed: {}", e)]),
}
}
pub fn validate_openapi_operation_security(
spec: &OpenApiSpec,
path: &str,
method: &str,
auth_header: Option<&str>,
api_key: Option<&str>,
) -> ValidationResult {
let operations = spec.operations_for_path(path);
let operation = operations
.iter()
.find(|(op_method, _)| op_method.to_uppercase() == method.to_uppercase());
let operation = match operation {
Some((_, op)) => op,
None => {
return ValidationResult::failure(vec![format!(
"Operation not found: {} {}",
method, path
)])
}
};
let openapi_operation =
OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
if let Some(ref security_reqs) = openapi_operation.security {
if !security_reqs.is_empty() {
return validate_openapi_security(spec, security_reqs, auth_header, api_key);
}
}
let global_security = spec.get_global_security_requirements();
if !global_security.is_empty() {
return validate_openapi_security(spec, &global_security, auth_header, api_key);
}
ValidationResult::success()
}
pub fn sanitize_html(input: &str) -> String {
input
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
.replace('/', "/")
}
pub fn validate_safe_path(path: &str) -> Result<String> {
if path.contains('\0') {
return Err(Error::validation("Path contains null bytes".to_string()));
}
if path.contains("..") {
return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
}
if path.contains('~') {
return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
}
if path.starts_with('/') {
return Err(Error::validation("Absolute paths not allowed".to_string()));
}
if path.len() >= 2 && path.chars().nth(1) == Some(':') {
return Err(Error::validation("Absolute paths with drive letters not allowed".to_string()));
}
if path.starts_with("\\\\") || path.starts_with("//") {
return Err(Error::validation("UNC paths not allowed".to_string()));
}
let normalized = path.replace('\\', "/");
if normalized.contains("//") {
return Err(Error::validation("Path contains empty segments".to_string()));
}
Ok(normalized)
}
pub fn sanitize_sql(input: &str) -> String {
input.replace('\'', "''")
}
pub fn validate_command_arg(arg: &str) -> Result<String> {
let dangerous_chars = [
'|', ';', '&', '<', '>', '`', '$', '(', ')', '*', '?', '[', ']', '{', '}', '~', '!', '\n',
'\r', '\0',
];
for ch in dangerous_chars.iter() {
if arg.contains(*ch) {
return Err(Error::validation(format!(
"Command argument contains dangerous character: '{}'",
ch
)));
}
}
if arg.contains("$(") {
return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
}
Ok(arg.to_string())
}
pub fn sanitize_json_string(input: &str) -> String {
input
.replace('\\', "\\\\") .replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn validate_url_safe(url: &str) -> Result<String> {
let url_lower = url.to_lowercase();
let localhost_patterns = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"];
for pattern in localhost_patterns.iter() {
if url_lower.contains(pattern) {
return Err(Error::validation(
"URLs pointing to localhost are not allowed".to_string(),
));
}
}
let private_ranges = [
"10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.",
"172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.",
"172.31.", "192.168.",
];
for range in private_ranges.iter() {
if url_lower.contains(range) {
return Err(Error::validation(format!(
"URLs pointing to private IP range '{}' are not allowed",
range
)));
}
}
if url_lower.contains("169.254.") {
return Err(Error::validation(
"URLs pointing to link-local addresses (169.254.x) are not allowed".to_string(),
));
}
let metadata_endpoints = [
"metadata.google.internal",
"169.254.169.254", "fd00:ec2::254", ];
for endpoint in metadata_endpoints.iter() {
if url_lower.contains(endpoint) {
return Err(Error::validation(format!(
"URLs pointing to cloud metadata endpoint '{}' are not allowed",
endpoint
)));
}
}
Ok(url.to_string())
}
pub fn sanitize_header_value(input: &str) -> String {
input.replace(['\r', '\n'], "").trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_result_success() {
let result = ValidationResult::success();
assert!(result.valid);
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_validation_result_failure() {
let errors = vec!["error1".to_string(), "error2".to_string()];
let result = ValidationResult::failure(errors.clone());
assert!(!result.valid);
assert_eq!(result.errors, errors);
assert!(result.warnings.is_empty());
}
#[test]
fn test_validation_result_with_warning() {
let result = ValidationResult::success()
.with_warning("warning1".to_string())
.with_warning("warning2".to_string());
assert!(result.valid);
assert_eq!(result.warnings.len(), 2);
}
#[test]
fn test_validator_from_json_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let validator = Validator::from_json_schema(&schema);
assert!(validator.is_ok());
assert!(validator.unwrap().is_implemented());
}
#[test]
fn test_validator_from_json_schema_invalid() {
let schema = json!({
"type": "invalid_type"
});
let validator = Validator::from_json_schema(&schema);
assert!(validator.is_err());
}
#[test]
fn test_validator_validate_json_schema_success() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!({"name": "test"});
assert!(validator.validate(&data).is_ok());
}
#[test]
fn test_validator_validate_json_schema_failure() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!({"name": 123});
assert!(validator.validate(&data).is_err());
}
#[test]
fn test_validator_from_openapi() {
let spec = json!({
"openapi": "3.0.0",
"info": {"title": "Test", "version": "1.0.0"},
"paths": {}
});
let validator = Validator::from_openapi(&spec);
assert!(validator.is_ok());
}
#[test]
fn test_validator_from_openapi_unsupported_version() {
let spec = json!({
"openapi": "2.0.0",
"info": {"title": "Test", "version": "1.0.0"},
"paths": {}
});
let validator = Validator::from_openapi(&spec);
assert!(validator.is_err());
}
#[test]
fn test_validator_validate_openapi() {
let spec = json!({
"openapi": "3.0.0",
"info": {"title": "Test", "version": "1.0.0"},
"paths": {}
});
let validator = Validator::from_openapi(&spec).unwrap();
let data = json!({"key": "value"});
assert!(validator.validate(&data).is_ok());
}
#[test]
fn test_validator_validate_openapi_non_object() {
let spec = json!({
"openapi": "3.0.0",
"info": {"title": "Test", "version": "1.0.0"},
"paths": {}
});
let validator = Validator::from_openapi(&spec).unwrap();
let data = json!("string");
assert!(validator.validate(&data).is_err());
}
#[test]
fn test_validate_json_schema_function() {
let schema = json!({
"type": "object",
"properties": {
"age": {"type": "number"}
}
});
let data = json!({"age": 25});
let result = validate_json_schema(&data, &schema);
assert!(result.valid);
let data = json!({"age": "25"});
let result = validate_json_schema(&data, &schema);
assert!(!result.valid);
}
#[test]
fn test_validate_openapi_function() {
let spec = json!({
"openapi": "3.0.0",
"info": {"title": "Test", "version": "1.0.0"},
"paths": {}
});
let data = json!({"test": "value"});
let result = validate_openapi(&data, &spec);
assert!(result.valid);
}
#[test]
fn test_validate_openapi_missing_fields() {
let spec = json!({
"openapi": "3.0.0"
});
let data = json!({});
let result = validate_openapi(&data, &spec);
assert!(!result.valid);
assert!(!result.errors.is_empty());
}
#[test]
fn test_validate_number_constraints_multiple_of() {
let schema = json!({
"type": "number",
"multipleOf": 5.0
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!(10);
assert!(validator.validate(&data).is_ok());
let data = json!(11);
let _ = validator.validate(&data);
}
#[test]
fn test_validate_array_constraints_min_items() {
let schema = json!({
"type": "array",
"minItems": 2
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!([1, 2]);
assert!(validator.validate(&data).is_ok());
let data = json!([1]);
assert!(validator.validate(&data).is_err());
}
#[test]
fn test_validate_array_constraints_max_items() {
let schema = json!({
"type": "array",
"maxItems": 2
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!([1]);
assert!(validator.validate(&data).is_ok());
let data = json!([1, 2, 3]);
assert!(validator.validate(&data).is_err());
}
#[test]
fn test_validate_array_unique_items() {
let schema = json!({
"type": "array",
"uniqueItems": true
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!([1, 2, 3]);
assert!(validator.validate(&data).is_ok());
let data = json!([1, 2, 2]);
assert!(validator.validate(&data).is_err());
}
#[test]
fn test_validate_object_required_properties() {
let schema = json!({
"type": "object",
"required": ["name", "age"]
});
let validator = Validator::from_json_schema(&schema).unwrap();
let data = json!({"name": "test", "age": 25});
assert!(validator.validate(&data).is_ok());
let data = json!({"name": "test"});
assert!(validator.validate(&data).is_err());
}
#[test]
fn test_validate_content_encoding_base64() {
let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
assert!(result.is_ok());
let result = validator.validate_content_encoding(Some("not-base64!@#"), "base64", "test");
assert!(result.is_err());
}
#[test]
fn test_validate_content_encoding_hex() {
let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
assert!(result.is_ok());
let result = validator.validate_content_encoding(Some("xyz"), "hex", "test");
assert!(result.is_err());
}
#[test]
fn test_has_unique_items() {
let validator = Validator::from_json_schema(&json!({})).unwrap();
let arr = vec![json!(1), json!(2), json!(3)];
assert!(validator.has_unique_items(&arr));
let arr = vec![json!(1), json!(2), json!(1)];
assert!(!validator.has_unique_items(&arr));
}
#[test]
fn test_validate_protobuf() {
let result = validate_protobuf(&[], &[]);
assert!(!result.valid);
assert!(!result.errors.is_empty());
}
#[test]
fn test_validate_protobuf_with_type() {
let result = validate_protobuf_with_type(&[], &[], "TestMessage");
assert!(!result.valid);
assert!(!result.errors.is_empty());
}
#[test]
fn test_is_implemented() {
let json_validator = Validator::from_json_schema(&json!({"type": "object"})).unwrap();
assert!(json_validator.is_implemented());
let openapi_validator = Validator::from_openapi(&json!({
"openapi": "3.0.0",
"info": {"title": "Test", "version": "1.0.0"},
"paths": {}
}))
.unwrap();
assert!(openapi_validator.is_implemented());
}
#[test]
fn test_sanitize_html() {
assert_eq!(
sanitize_html("<script>alert('xss')</script>"),
"<script>alert('xss')</script>"
);
assert_eq!(
sanitize_html("<img src=x onerror=\"alert(1)\">"),
"<img src=x onerror="alert(1)">"
);
assert_eq!(
sanitize_html("<a href=\"javascript:void(0)\">"),
"<a href="javascript:void(0)">"
);
assert_eq!(sanitize_html("&<>"), "&<>");
assert_eq!(
sanitize_html("Hello <b>World</b> & 'Friends'"),
"Hello <b>World</b> & 'Friends'"
);
}
#[test]
fn test_validate_safe_path() {
assert!(validate_safe_path("data/file.txt").is_ok());
assert!(validate_safe_path("subdir/file.json").is_ok());
assert!(validate_safe_path("file.txt").is_ok());
assert!(validate_safe_path("../etc/passwd").is_err());
assert!(validate_safe_path("dir/../../../etc/passwd").is_err());
assert!(validate_safe_path("./../../secret").is_err());
assert!(validate_safe_path("~/secret").is_err());
assert!(validate_safe_path("dir/~/file").is_err());
assert!(validate_safe_path("/etc/passwd").is_err());
assert!(validate_safe_path("/var/log/app.log").is_err());
assert!(validate_safe_path("C:\\Windows\\System32").is_err());
assert!(validate_safe_path("D:\\data\\file.txt").is_err());
assert!(validate_safe_path("\\\\server\\share").is_err());
assert!(validate_safe_path("//server/share").is_err());
assert!(validate_safe_path("file\0.txt").is_err());
assert!(validate_safe_path("dir//file.txt").is_err());
let result = validate_safe_path("dir\\subdir\\file.txt").unwrap();
assert_eq!(result, "dir/subdir/file.txt");
}
#[test]
fn test_sanitize_sql() {
assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
assert_eq!(sanitize_sql("admin"), "admin");
assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
}
#[test]
fn test_validate_command_arg() {
assert!(validate_command_arg("safe_filename.txt").is_ok());
assert!(validate_command_arg("file-123.log").is_ok());
assert!(validate_command_arg("data.json").is_ok());
assert!(validate_command_arg("file | cat /etc/passwd").is_err());
assert!(validate_command_arg("file || echo pwned").is_err());
assert!(validate_command_arg("file; rm -rf /").is_err());
assert!(validate_command_arg("file & background").is_err());
assert!(validate_command_arg("file && next").is_err());
assert!(validate_command_arg("file > /dev/null").is_err());
assert!(validate_command_arg("file < input.txt").is_err());
assert!(validate_command_arg("file >> log.txt").is_err());
assert!(validate_command_arg("file `whoami`").is_err());
assert!(validate_command_arg("file $(whoami)").is_err());
assert!(validate_command_arg("file*.txt").is_err());
assert!(validate_command_arg("file?.log").is_err());
assert!(validate_command_arg("file[0-9]").is_err());
assert!(validate_command_arg("file{1,2}").is_err());
assert!(validate_command_arg("file\0.txt").is_err());
assert!(validate_command_arg("file\nrm -rf /").is_err());
assert!(validate_command_arg("file\rcommand").is_err());
assert!(validate_command_arg("file~").is_err());
assert!(validate_command_arg("file!").is_err());
}
#[test]
fn test_sanitize_json_string() {
assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
assert_eq!(sanitize_json_string("line1\nline2"), r#"line1\nline2"#);
assert_eq!(sanitize_json_string("tab\there"), r#"tab\there"#);
assert_eq!(sanitize_json_string("carriage\rreturn"), r#"carriage\rreturn"#);
assert_eq!(
sanitize_json_string("Test\"value\"\nNext\\line"),
r#"Test\"value\"\nNext\\line"#
);
}
#[test]
fn test_validate_url_safe() {
assert!(validate_url_safe("https://example.com").is_ok());
assert!(validate_url_safe("http://api.example.com/data").is_ok());
assert!(validate_url_safe("https://subdomain.example.org:8080/path").is_ok());
assert!(validate_url_safe("http://localhost:8080").is_err());
assert!(validate_url_safe("http://127.0.0.1").is_err());
assert!(validate_url_safe("http://[::1]:8080").is_err());
assert!(validate_url_safe("http://0.0.0.0").is_err());
assert!(validate_url_safe("http://10.0.0.1").is_err());
assert!(validate_url_safe("http://192.168.1.1").is_err());
assert!(validate_url_safe("http://172.16.0.1").is_err());
assert!(validate_url_safe("http://172.31.255.255").is_err());
assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
assert!(validate_url_safe("http://metadata.google.internal").is_err());
assert!(validate_url_safe("http://169.254.169.254").is_err());
assert!(validate_url_safe("HTTP://LOCALHOST:8080").is_err());
assert!(validate_url_safe("http://LocalHost").is_err());
}
#[test]
fn test_sanitize_header_value() {
let malicious = "value\r\nX-Evil-Header: injected";
let safe = sanitize_header_value(malicious);
assert!(!safe.contains('\r'));
assert!(!safe.contains('\n'));
assert_eq!(safe, "valueX-Evil-Header: injected");
let malicious = "session123\r\nSet-Cookie: admin=true";
let safe = sanitize_header_value(malicious);
assert_eq!(safe, "session123Set-Cookie: admin=true");
assert_eq!(sanitize_header_value(" value "), "value");
let malicious = "val\nue\r\nhe\na\rder";
let safe = sanitize_header_value(malicious);
assert_eq!(safe, "valueheader");
assert_eq!(sanitize_header_value("clean-value-123"), "clean-value-123");
}
#[test]
fn test_sanitize_html_empty_and_whitespace() {
assert_eq!(sanitize_html(""), "");
assert_eq!(sanitize_html(" "), " ");
}
#[test]
fn test_validate_safe_path_edge_cases() {
assert!(validate_safe_path(".").is_ok());
assert!(validate_safe_path("README.md").is_ok());
assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
assert!(validate_safe_path("file.test.txt").is_ok());
assert!(validate_safe_path("..").is_err());
assert!(validate_safe_path("dir/..").is_err());
}
#[test]
fn test_sanitize_sql_edge_cases() {
assert_eq!(sanitize_sql(""), "");
assert_eq!(sanitize_sql("''"), "''''");
assert_eq!(sanitize_sql("'''"), "''''''");
}
#[test]
fn test_validate_command_arg_edge_cases() {
assert!(validate_command_arg("").is_ok());
assert!(validate_command_arg("file_name-123").is_ok());
assert!(validate_command_arg("12345").is_ok());
}
}