Skip to main content

mockforge_core/
validation.rs

1//! Pillars: [Contracts]
2//!
3//! Schema validation logic for MockForge
4
5use crate::{
6    openapi::{OpenApiOperation, OpenApiSecurityRequirement, OpenApiSpec},
7    Error, Result,
8};
9use base32::Alphabet;
10use base64::{engine::general_purpose, Engine as _};
11use jsonschema::{self, Draft, Validator as JSONSchema};
12use prost_reflect::{DescriptorPool, DynamicMessage};
13use serde_json::{json, Value};
14
15/// Schema validator for different formats
16#[derive(Debug)]
17pub enum Validator {
18    /// JSON Schema validator
19    JsonSchema(JSONSchema),
20    /// OpenAPI 3.1 schema validator with original schema for extensions
21    OpenApi31Schema(JSONSchema, Value),
22    /// OpenAPI schema validator
23    OpenApi(Box<OpenApiSpec>),
24    /// Protobuf validator with descriptor pool
25    Protobuf(DescriptorPool),
26}
27
28impl Validator {
29    /// Create a JSON Schema validator from a schema
30    pub fn from_json_schema(schema: &Value) -> Result<Self> {
31        let compiled = jsonschema::options()
32            .with_draft(Draft::Draft7)
33            .build(schema)
34            .map_err(|e| Error::validation(format!("Failed to compile JSON schema: {}", e)))?;
35
36        Ok(Self::JsonSchema(compiled))
37    }
38
39    /// Create a validator that supports OpenAPI 3.1 features from a schema
40    pub fn from_openapi31_schema(schema: &Value) -> Result<Self> {
41        let compiled =
42            jsonschema::options().with_draft(Draft::Draft7).build(schema).map_err(|e| {
43                Error::validation(format!("Failed to compile OpenAPI 3.1 schema: {}", e))
44            })?;
45
46        Ok(Self::OpenApi31Schema(compiled, schema.clone()))
47    }
48
49    /// Create an OpenAPI validator
50    pub fn from_openapi(spec: &Value) -> Result<Self> {
51        // Validate that it's a valid OpenAPI spec
52        if let Some(openapi_version) = spec.get("openapi") {
53            if let Some(version_str) = openapi_version.as_str() {
54                if !version_str.starts_with("3.") {
55                    return Err(Error::validation(format!(
56                        "Unsupported OpenAPI version: {}. Only 3.x is supported",
57                        version_str
58                    )));
59                }
60            }
61        }
62
63        // Parse and store the spec for advanced validation
64        let openapi_spec = OpenApiSpec::from_json(spec.clone())
65            .map_err(|e| Error::validation(format!("Failed to parse OpenAPI spec: {}", e)))?;
66
67        Ok(Self::OpenApi(Box::new(openapi_spec)))
68    }
69
70    /// Create a Protobuf validator from descriptor bytes
71    pub fn from_protobuf(descriptor: &[u8]) -> Result<Self> {
72        let mut pool = DescriptorPool::new();
73        pool.decode_file_descriptor_set(descriptor)
74            .map_err(|e| Error::validation(format!("Invalid protobuf descriptor: {}", e)))?;
75        Ok(Self::Protobuf(pool))
76    }
77
78    /// Validate data against the schema
79    pub fn validate(&self, data: &Value) -> Result<()> {
80        match self {
81            Self::JsonSchema(schema) => {
82                let mut errors = Vec::new();
83                for error in schema.iter_errors(data) {
84                    errors.push(error.to_string());
85                }
86
87                if errors.is_empty() {
88                    Ok(())
89                } else {
90                    Err(Error::validation(format!("Validation failed: {}", errors.join(", "))))
91                }
92            }
93            Self::OpenApi31Schema(schema, original_schema) => {
94                // First validate with standard JSON Schema
95                let mut errors = Vec::new();
96                for error in schema.iter_errors(data) {
97                    errors.push(error.to_string());
98                }
99
100                if !errors.is_empty() {
101                    return Err(Error::validation(format!(
102                        "Validation failed: {}",
103                        errors.join(", ")
104                    )));
105                }
106
107                // Then validate OpenAPI 3.1 extensions
108                self.validate_openapi31_schema(data, original_schema)
109            }
110            Self::OpenApi(_spec) => {
111                // Use the stored spec for advanced validation
112                if data.is_object() {
113                    // For now, perform basic validation - could be extended to validate against specific schemas
114                    Ok(())
115                } else {
116                    Err(Error::validation("OpenAPI validation expects an object".to_string()))
117                }
118            }
119            Self::Protobuf(_) => {
120                // Protobuf validation cannot be performed via the JSON-based validate() method.
121                // Use validate_protobuf(), validate_protobuf_message(), or
122                // validate_protobuf_with_type() with raw bytes instead.
123                Err(Error::validation(
124                    "Protobuf validation requires binary data — use validate_protobuf() functions directly".to_string()
125                ))
126            }
127        }
128    }
129
130    /// Check if validation is supported for this validator type
131    pub fn is_implemented(&self) -> bool {
132        match self {
133            Self::JsonSchema(_) => true,
134            Self::OpenApi31Schema(_, _) => true,
135            Self::OpenApi(_) => true, // Now implemented with schema validation
136            Self::Protobuf(_) => false, // JSON-based validate() not supported; use validate_protobuf() functions
137        }
138    }
139
140    /// Enhanced validation with OpenAPI 3.1 support
141    pub fn validate_openapi_ext(&self, data: &Value, openapi_schema: &Value) -> Result<()> {
142        match self {
143            Self::JsonSchema(_) => {
144                // For OpenAPI 3.1, we need enhanced validation beyond JSON Schema Draft 7
145                self.validate_openapi31_schema(data, openapi_schema)
146            }
147            Self::OpenApi31Schema(_, _) => {
148                // For OpenAPI 3.1 schemas, use the enhanced validation
149                self.validate_openapi31_schema(data, openapi_schema)
150            }
151            Self::OpenApi(_spec) => {
152                // Basic OpenAPI validation - for now just check if it's valid JSON
153                if data.is_object() {
154                    Ok(())
155                } else {
156                    Err(Error::validation("OpenAPI validation expects an object".to_string()))
157                }
158            }
159            Self::Protobuf(_) => {
160                Err(Error::validation(
161                    "Protobuf validation requires binary data — use validate_protobuf() functions directly".to_string()
162                ))
163            }
164        }
165    }
166
167    /// Validate data against OpenAPI 3.1 schema constraints
168    fn validate_openapi31_schema(&self, data: &Value, schema: &Value) -> Result<()> {
169        self.validate_openapi31_constraints(data, schema, "")
170    }
171
172    /// Recursively validate OpenAPI 3.1 schema constraints
173    fn validate_openapi31_constraints(
174        &self,
175        data: &Value,
176        schema: &Value,
177        path: &str,
178    ) -> Result<()> {
179        let schema_obj = schema
180            .as_object()
181            .ok_or_else(|| Error::validation(format!("{}: Schema must be an object", path)))?;
182
183        // Handle type-specific validation
184        if let Some(type_str) = schema_obj.get("type").and_then(|v| v.as_str()) {
185            match type_str {
186                "number" | "integer" => self.validate_number_constraints(data, schema_obj, path)?,
187                "array" => self.validate_array_constraints(data, schema_obj, path)?,
188                "object" => self.validate_object_constraints(data, schema_obj, path)?,
189                "string" => self.validate_string_constraints(data, schema_obj, path)?,
190                _ => {} // Other types handled by base JSON Schema validation
191            }
192        }
193
194        // Handle allOf, anyOf, oneOf composition
195        if let Some(all_of) = schema_obj.get("allOf").and_then(|v| v.as_array()) {
196            for subschema in all_of {
197                self.validate_openapi31_constraints(data, subschema, path)?;
198            }
199        }
200
201        if let Some(any_of) = schema_obj.get("anyOf").and_then(|v| v.as_array()) {
202            let mut errors = Vec::new();
203            for subschema in any_of {
204                if let Err(e) = self.validate_openapi31_constraints(data, subschema, path) {
205                    errors.push(e.to_string());
206                } else {
207                    // At least one subschema matches
208                    return Ok(());
209                }
210            }
211            if !errors.is_empty() {
212                return Err(Error::validation(format!(
213                    "{}: No subschema in anyOf matched: {}",
214                    path,
215                    errors.join(", ")
216                )));
217            }
218        }
219
220        if let Some(one_of) = schema_obj.get("oneOf").and_then(|v| v.as_array()) {
221            let mut matches = 0;
222            for subschema in one_of {
223                if self.validate_openapi31_constraints(data, subschema, path).is_ok() {
224                    matches += 1;
225                }
226            }
227            if matches != 1 {
228                return Err(Error::validation(format!(
229                    "{}: Expected exactly one subschema in oneOf to match, got {}",
230                    path, matches
231                )));
232            }
233        }
234
235        // Handle contentEncoding
236        if let Some(content_encoding) = schema_obj.get("contentEncoding").and_then(|v| v.as_str()) {
237            self.validate_content_encoding(data.as_str(), content_encoding, path)?;
238        }
239
240        Ok(())
241    }
242
243    /// Validate number-specific OpenAPI 3.1 constraints
244    fn validate_number_constraints(
245        &self,
246        data: &Value,
247        schema: &serde_json::Map<String, Value>,
248        path: &str,
249    ) -> Result<()> {
250        let num = data
251            .as_f64()
252            .ok_or_else(|| Error::validation(format!("{}: Expected number, got {}", path, data)))?;
253
254        // multipleOf validation
255        if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
256            if multiple_of > 0.0 && (num / multiple_of) % 1.0 != 0.0 {
257                return Err(Error::validation(format!(
258                    "{}: {} is not a multiple of {}",
259                    path, num, multiple_of
260                )));
261            }
262        }
263
264        // exclusiveMinimum validation
265        if let Some(excl_min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
266            if num <= excl_min {
267                return Err(Error::validation(format!(
268                    "{}: {} must be greater than {}",
269                    path, num, excl_min
270                )));
271            }
272        }
273
274        // exclusiveMaximum validation
275        if let Some(excl_max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
276            if num >= excl_max {
277                return Err(Error::validation(format!(
278                    "{}: {} must be less than {}",
279                    path, num, excl_max
280                )));
281            }
282        }
283
284        Ok(())
285    }
286
287    /// Validate array-specific OpenAPI 3.1 constraints
288    fn validate_array_constraints(
289        &self,
290        data: &Value,
291        schema: &serde_json::Map<String, Value>,
292        path: &str,
293    ) -> Result<()> {
294        let arr = data
295            .as_array()
296            .ok_or_else(|| Error::validation(format!("{}: Expected array, got {}", path, data)))?;
297
298        // minItems validation
299        if let Some(min_items) = schema.get("minItems").and_then(|v| v.as_u64()).map(|v| v as usize)
300        {
301            if arr.len() < min_items {
302                return Err(Error::validation(format!(
303                    "{}: Array has {} items, minimum is {}",
304                    path,
305                    arr.len(),
306                    min_items
307                )));
308            }
309        }
310
311        // maxItems validation
312        if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()).map(|v| v as usize)
313        {
314            if arr.len() > max_items {
315                return Err(Error::validation(format!(
316                    "{}: Array has {} items, maximum is {}",
317                    path,
318                    arr.len(),
319                    max_items
320                )));
321            }
322        }
323
324        // uniqueItems validation
325        if let Some(unique) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
326            if unique && !self.has_unique_items(arr) {
327                return Err(Error::validation(format!("{}: Array items must be unique", path)));
328            }
329        }
330
331        // Validate items if schema is provided
332        if let Some(items_schema) = schema.get("items") {
333            for (idx, item) in arr.iter().enumerate() {
334                let item_path = format!("{}[{}]", path, idx);
335                self.validate_openapi31_constraints(item, items_schema, &item_path)?;
336            }
337        }
338
339        Ok(())
340    }
341
342    /// Validate object-specific OpenAPI 3.1 constraints
343    fn validate_object_constraints(
344        &self,
345        data: &Value,
346        schema: &serde_json::Map<String, Value>,
347        path: &str,
348    ) -> Result<()> {
349        let obj = data
350            .as_object()
351            .ok_or_else(|| Error::validation(format!("{}: Expected object, got {}", path, data)))?;
352
353        // Required properties
354        if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
355            for req_prop in required {
356                if let Some(prop_name) = req_prop.as_str() {
357                    if !obj.contains_key(prop_name) {
358                        return Err(Error::validation(format!(
359                            "{}: Missing required property '{}'",
360                            path, prop_name
361                        )));
362                    }
363                }
364            }
365        }
366
367        // Properties validation
368        if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
369            for (prop_name, prop_schema) in properties {
370                if let Some(prop_value) = obj.get(prop_name) {
371                    let prop_path = format!("{}/{}", path, prop_name);
372                    self.validate_openapi31_constraints(prop_value, prop_schema, &prop_path)?;
373                }
374            }
375        }
376
377        Ok(())
378    }
379
380    /// Validate string-specific OpenAPI 3.1 constraints
381    fn validate_string_constraints(
382        &self,
383        data: &Value,
384        schema: &serde_json::Map<String, Value>,
385        path: &str,
386    ) -> Result<()> {
387        let _str_val = data
388            .as_str()
389            .ok_or_else(|| Error::validation(format!("{}: Expected string, got {}", path, data)))?;
390
391        // Content encoding validation (handled separately in validate_content_encoding)
392        // but we ensure it's a string for encoding validation
393        if schema.get("contentEncoding").is_some() {
394            // Content encoding validation is handled by validate_content_encoding
395        }
396
397        Ok(())
398    }
399
400    /// Validate content encoding
401    fn validate_content_encoding(
402        &self,
403        data: Option<&str>,
404        encoding: &str,
405        path: &str,
406    ) -> Result<()> {
407        let str_data = data.ok_or_else(|| {
408            Error::validation(format!("{}: Content encoding requires string data", path))
409        })?;
410
411        match encoding {
412            "base64" => {
413                if general_purpose::STANDARD.decode(str_data).is_err() {
414                    return Err(Error::validation(format!("{}: Invalid base64 encoding", path)));
415                }
416            }
417            "base64url" => {
418                use base64::engine::general_purpose::URL_SAFE;
419                use base64::Engine;
420                if URL_SAFE.decode(str_data).is_err() {
421                    return Err(Error::validation(format!("{}: Invalid base64url encoding", path)));
422                }
423            }
424            "base32" => {
425                if base32::decode(Alphabet::Rfc4648 { padding: false }, str_data).is_none() {
426                    return Err(Error::validation(format!("{}: Invalid base32 encoding", path)));
427                }
428            }
429            "hex" | "binary" => {
430                if hex::decode(str_data).is_err() {
431                    return Err(Error::validation(format!(
432                        "{}: Invalid {} encoding",
433                        path, encoding
434                    )));
435                }
436            }
437            // Other encodings could be added here (gzip, etc.)
438            _ => {
439                // Unknown encoding - log a warning but don't fail validation
440                tracing::warn!(
441                    "{}: Unknown content encoding '{}', skipping validation",
442                    path,
443                    encoding
444                );
445            }
446        }
447
448        Ok(())
449    }
450
451    /// Check if array has unique items
452    fn has_unique_items(&self, arr: &[Value]) -> bool {
453        let mut seen = std::collections::HashSet::new();
454        for item in arr {
455            let item_str = serde_json::to_string(item).unwrap_or_default();
456            if !seen.insert(item_str) {
457                return false;
458            }
459        }
460        true
461    }
462}
463
464/// Validation result with detailed error information
465#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
466pub struct ValidationResult {
467    /// Whether validation passed
468    pub valid: bool,
469    /// Validation errors (empty if valid)
470    pub errors: Vec<String>,
471    /// Validation warnings
472    pub warnings: Vec<String>,
473}
474
475impl ValidationResult {
476    /// Create a successful validation result
477    pub fn success() -> Self {
478        Self {
479            valid: true,
480            errors: Vec::new(),
481            warnings: Vec::new(),
482        }
483    }
484
485    /// Create a failed validation result
486    pub fn failure(errors: Vec<String>) -> Self {
487        Self {
488            valid: false,
489            errors,
490            warnings: Vec::new(),
491        }
492    }
493
494    /// Add a warning to the result
495    pub fn with_warning(mut self, warning: String) -> Self {
496        self.warnings.push(warning);
497        self
498    }
499}
500
501/// Validate JSON data against a JSON Schema
502pub fn validate_json_schema(data: &Value, schema: &Value) -> ValidationResult {
503    match Validator::from_json_schema(schema) {
504        Ok(validator) => match validator.validate(data) {
505            Ok(_) => ValidationResult::success(),
506            Err(Error::Validation { message }) => ValidationResult::failure(vec![message]),
507            Err(e) => ValidationResult::failure(vec![format!("Unexpected error: {}", e)]),
508        },
509        Err(e) => ValidationResult::failure(vec![format!("Schema compilation error: {}", e)]),
510    }
511}
512
513/// Validate OpenAPI spec compliance
514pub fn validate_openapi(data: &Value, spec: &Value) -> ValidationResult {
515    // Basic validation - check if the spec has required OpenAPI fields
516    let spec_obj = match spec.as_object() {
517        Some(obj) => obj,
518        None => {
519            return ValidationResult::failure(vec!["OpenAPI spec must be an object".to_string()])
520        }
521    };
522
523    // Check required fields
524    let mut errors = Vec::new();
525
526    if !spec_obj.contains_key("openapi") {
527        errors.push("Missing required 'openapi' field".to_string());
528    } else if let Some(version) = spec_obj.get("openapi").and_then(|v| v.as_str()) {
529        if !version.starts_with("3.") {
530            errors.push(format!("Unsupported OpenAPI version: {}. Only 3.x is supported", version));
531        }
532    }
533
534    if !spec_obj.contains_key("info") {
535        errors.push("Missing required 'info' field".to_string());
536    } else if let Some(info) = spec_obj.get("info").and_then(|v| v.as_object()) {
537        if !info.contains_key("title") {
538            errors.push("Missing required 'info.title' field".to_string());
539        }
540        if !info.contains_key("version") {
541            errors.push("Missing required 'info.version' field".to_string());
542        }
543    }
544
545    if !spec_obj.contains_key("paths") {
546        errors.push("Missing required 'paths' field".to_string());
547    }
548
549    if !errors.is_empty() {
550        return ValidationResult::failure(errors);
551    }
552
553    // Now perform actual schema validation if possible
554    if serde_json::from_value::<openapiv3::OpenAPI>(spec.clone()).is_ok() {
555        let _spec_wrapper = OpenApiSpec::from_json(spec.clone()).unwrap_or_else(|_| {
556            // Fallback to empty spec on error - this should never happen with valid JSON
557            OpenApiSpec::from_json(json!({}))
558                .expect("Empty JSON object should always create valid OpenApiSpec")
559        });
560
561        // Try to validate the data against the spec
562        // For now, we'll do a basic check to see if the data structure is reasonable
563        if data.is_object() {
564            // If we have a properly parsed spec, we could do more detailed validation here
565            // For backward compatibility, we'll mark this as successful but note the limitation
566            ValidationResult::success()
567                .with_warning("OpenAPI schema validation available - use validate_openapi_with_path for operation-specific validation".to_string())
568        } else {
569            ValidationResult::failure(vec![
570                "Request/response data must be a JSON object".to_string()
571            ])
572        }
573    } else {
574        ValidationResult::failure(vec!["Failed to parse OpenAPI specification".to_string()])
575    }
576}
577
578/// Validate data against a specific OpenAPI operation schema
579pub fn validate_openapi_operation(
580    _data: &Value,
581    spec: &OpenApiSpec,
582    path: &str,
583    method: &str,
584    _is_request: bool,
585) -> ValidationResult {
586    let mut errors = Vec::new();
587
588    // Try to find the operation in the spec
589    if let Some(path_item_ref) = spec.spec.paths.paths.get(path) {
590        // Handle ReferenceOr<PathItem>
591        if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
592            let operation = match method.to_uppercase().as_str() {
593                "GET" => path_item.get.as_ref(),
594                "POST" => path_item.post.as_ref(),
595                "PUT" => path_item.put.as_ref(),
596                "DELETE" => path_item.delete.as_ref(),
597                "PATCH" => path_item.patch.as_ref(),
598                "HEAD" => path_item.head.as_ref(),
599                "OPTIONS" => path_item.options.as_ref(),
600                _ => None,
601            };
602
603            if operation.is_some() {
604                // Note: Schema validation is handled in validate_openapi_with_path function
605                // This function focuses on basic spec structure validation
606            } else {
607                errors.push(format!("Method {} not found for path {}", method, path));
608            }
609        } else {
610            errors
611                .push(format!("Path {} contains a reference, not supported for validation", path));
612        }
613    } else {
614        errors.push(format!("Path {} not found in OpenAPI spec", path));
615    }
616
617    if errors.is_empty() {
618        ValidationResult::success()
619    } else {
620        ValidationResult::failure(errors)
621    }
622}
623
624/// Validate Protobuf message against schema
625pub fn validate_protobuf(data: &[u8], descriptor_data: &[u8]) -> ValidationResult {
626    let mut pool = DescriptorPool::new();
627    if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
628        return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
629    }
630
631    let Some(message_descriptor) = pool.all_messages().next() else {
632        return ValidationResult::failure(vec![
633            "Protobuf descriptor set does not contain any message descriptors".to_string(),
634        ]);
635    };
636
637    match DynamicMessage::decode(message_descriptor, data) {
638        Ok(_) => ValidationResult::success(),
639        Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
640    }
641}
642
643/// Validate protobuf data against a specific message descriptor
644pub fn validate_protobuf_message(
645    data: &[u8],
646    message_descriptor: &prost_reflect::MessageDescriptor,
647) -> Result<()> {
648    // Try to decode the data as the given message type
649    match DynamicMessage::decode(message_descriptor.clone(), data) {
650        Ok(_) => Ok(()),
651        Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
652    }
653}
654
655/// Validate protobuf data with explicit message type name
656pub fn validate_protobuf_with_type(
657    data: &[u8],
658    descriptor_data: &[u8],
659    message_type_name: &str,
660) -> ValidationResult {
661    let mut pool = DescriptorPool::new();
662    if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
663        return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
664    }
665
666    let descriptor = pool.get_message_by_name(message_type_name).or_else(|| {
667        pool.all_messages().find(|msg| {
668            msg.name() == message_type_name || msg.full_name().ends_with(message_type_name)
669        })
670    });
671
672    let Some(message_descriptor) = descriptor else {
673        return ValidationResult::failure(vec![format!(
674            "Message type '{}' not found in descriptor set",
675            message_type_name
676        )]);
677    };
678
679    match DynamicMessage::decode(message_descriptor, data) {
680        Ok(_) => ValidationResult::success(),
681        Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
682    }
683}
684
685/// Validate OpenAPI security requirements
686pub fn validate_openapi_security(
687    spec: &OpenApiSpec,
688    security_requirements: &[OpenApiSecurityRequirement],
689    auth_header: Option<&str>,
690    api_key: Option<&str>,
691) -> ValidationResult {
692    match spec.validate_security_requirements(security_requirements, auth_header, api_key) {
693        Ok(_) => ValidationResult::success(),
694        Err(e) => ValidationResult::failure(vec![format!("Security validation failed: {}", e)]),
695    }
696}
697
698/// Validate security for a specific OpenAPI operation
699pub fn validate_openapi_operation_security(
700    spec: &OpenApiSpec,
701    path: &str,
702    method: &str,
703    auth_header: Option<&str>,
704    api_key: Option<&str>,
705) -> ValidationResult {
706    // Get operations for this path
707    let operations = spec.operations_for_path(path);
708
709    // Find the specific operation
710    let operation = operations
711        .iter()
712        .find(|(op_method, _)| op_method.to_uppercase() == method.to_uppercase());
713
714    let operation = match operation {
715        Some((_, op)) => op,
716        None => {
717            return ValidationResult::failure(vec![format!(
718                "Operation not found: {} {}",
719                method, path
720            )])
721        }
722    };
723
724    // Convert operation to OpenApiOperation for security validation
725    let openapi_operation =
726        OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
727
728    // Check operation-specific security first
729    if let Some(ref security_reqs) = openapi_operation.security {
730        if !security_reqs.is_empty() {
731            return validate_openapi_security(spec, security_reqs, auth_header, api_key);
732        }
733    }
734
735    // Fall back to global security requirements
736    let global_security = spec.get_global_security_requirements();
737    if !global_security.is_empty() {
738        return validate_openapi_security(spec, &global_security, auth_header, api_key);
739    }
740
741    // No security requirements
742    ValidationResult::success()
743}
744
745// ============================================================================
746// INPUT SANITIZATION
747// ============================================================================
748
749/// Sanitize HTML to prevent XSS attacks
750///
751/// This function escapes HTML special characters to prevent script injection.
752/// Use this for any user-provided content that will be displayed in HTML contexts.
753///
754/// # Example
755/// ```
756/// use mockforge_core::validation::sanitize_html;
757///
758/// let malicious = "<script>alert('xss')</script>";
759/// let safe = sanitize_html(malicious);
760/// assert_eq!(safe, "&lt;script&gt;alert(&#39;xss&#39;)&lt;&#x2F;script&gt;");
761/// ```
762pub fn sanitize_html(input: &str) -> String {
763    input
764        .replace('&', "&amp;")
765        .replace('<', "&lt;")
766        .replace('>', "&gt;")
767        .replace('"', "&quot;")
768        .replace('\'', "&#39;")
769        .replace('/', "&#x2F;")
770}
771
772/// Validate and sanitize file paths to prevent path traversal attacks
773///
774/// This function checks for common path traversal patterns and returns an error
775/// if any are detected. It also normalizes the path to prevent bypass attempts.
776///
777/// # Security Concerns
778/// - Blocks `..` (parent directory)
779/// - Blocks `~` (home directory expansion)
780/// - Blocks absolute paths (starting with `/` or drive letters on Windows)
781/// - Blocks null bytes
782///
783/// # Example
784/// ```
785/// use mockforge_core::validation::validate_safe_path;
786///
787/// assert!(validate_safe_path("data/file.txt").is_ok());
788/// assert!(validate_safe_path("../etc/passwd").is_err());
789/// assert!(validate_safe_path("/etc/passwd").is_err());
790/// ```
791pub fn validate_safe_path(path: &str) -> Result<String> {
792    // Check for null bytes
793    if path.contains('\0') {
794        return Err(Error::validation("Path contains null bytes".to_string()));
795    }
796
797    // Check for path traversal attempts
798    if path.contains("..") {
799        return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
800    }
801
802    // Check for home directory expansion
803    if path.contains('~') {
804        return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
805    }
806
807    // Check for absolute paths (Unix)
808    if path.starts_with('/') {
809        return Err(Error::validation("Absolute paths not allowed".to_string()));
810    }
811
812    // Check for absolute paths (Windows drive letters)
813    if path.len() >= 2 && path.chars().nth(1) == Some(':') {
814        return Err(Error::validation("Absolute paths with drive letters not allowed".to_string()));
815    }
816
817    // Check for UNC paths (Windows network paths)
818    if path.starts_with("\\\\") || path.starts_with("//") {
819        return Err(Error::validation("UNC paths not allowed".to_string()));
820    }
821
822    // Normalize path separators to forward slashes
823    let normalized = path.replace('\\', "/");
824
825    // Additional check: ensure no empty segments (e.g., "foo//bar")
826    if normalized.contains("//") {
827        return Err(Error::validation("Path contains empty segments".to_string()));
828    }
829
830    Ok(normalized)
831}
832
833/// Sanitize SQL input to prevent SQL injection
834///
835/// This function escapes SQL special characters. However, **parameterized queries
836/// should always be preferred** over manual sanitization.
837///
838/// # Warning
839/// This is a last-resort defense. Always use parameterized queries when possible.
840///
841/// # Example
842/// ```
843/// use mockforge_core::validation::sanitize_sql;
844///
845/// let input = "admin' OR '1'='1";
846/// let safe = sanitize_sql(input);
847/// assert_eq!(safe, "admin'' OR ''1''=''1");
848/// ```
849pub fn sanitize_sql(input: &str) -> String {
850    // Escape single quotes by doubling them (SQL standard)
851    input.replace('\'', "''")
852}
853
854/// Validate command arguments to prevent command injection
855///
856/// This function checks for shell metacharacters and returns an error if any
857/// are detected. Use this when building shell commands from user input.
858///
859/// # Security Concerns
860/// Blocks the following shell metacharacters:
861/// - Pipes: `|`, `||`
862/// - Command separators: `;`, `&`, `&&`
863/// - Redirection: `<`, `>`, `>>`
864/// - Command substitution: `` ` ``, `$(`, `)`
865/// - Wildcards: `*`, `?`
866/// - Null byte: `\0`
867///
868/// # Example
869/// ```
870/// use mockforge_core::validation::validate_command_arg;
871///
872/// assert!(validate_command_arg("safe_filename.txt").is_ok());
873/// assert!(validate_command_arg("file; rm -rf /").is_err());
874/// assert!(validate_command_arg("file | cat /etc/passwd").is_err());
875/// ```
876pub fn validate_command_arg(arg: &str) -> Result<String> {
877    // List of dangerous shell metacharacters
878    let dangerous_chars = [
879        '|', ';', '&', '<', '>', '`', '$', '(', ')', '*', '?', '[', ']', '{', '}', '~', '!', '\n',
880        '\r', '\0',
881    ];
882
883    for ch in dangerous_chars.iter() {
884        if arg.contains(*ch) {
885            return Err(Error::validation(format!(
886                "Command argument contains dangerous character: '{}'",
887                ch
888            )));
889        }
890    }
891
892    // Check for command substitution patterns
893    if arg.contains("$(") {
894        return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
895    }
896
897    Ok(arg.to_string())
898}
899
900/// Sanitize JSON string values to prevent JSON injection
901///
902/// This function escapes special characters in JSON string values to prevent
903/// injection attacks when building JSON dynamically.
904///
905/// # Example
906/// ```
907/// use mockforge_core::validation::sanitize_json_string;
908///
909/// let input = r#"test","admin":true,"#;
910/// let safe = sanitize_json_string(input);
911/// assert!(safe.contains(r#"\""#));
912/// ```
913pub fn sanitize_json_string(input: &str) -> String {
914    input
915        .replace('\\', "\\\\") // Backslash must be first
916        .replace('"', "\\\"")
917        .replace('\n', "\\n")
918        .replace('\r', "\\r")
919        .replace('\t', "\\t")
920}
921
922/// Validate URL to prevent SSRF (Server-Side Request Forgery) attacks
923///
924/// This function checks URLs for private IP ranges, localhost, and metadata endpoints
925/// that could be exploited in SSRF attacks.
926///
927/// # Security Concerns
928/// - Blocks localhost (127.0.0.1, ::1, localhost)
929/// - Blocks private IP ranges (10.x, 172.16-31.x, 192.168.x)
930/// - Blocks link-local addresses (169.254.x)
931/// - Blocks cloud metadata endpoints
932///
933/// # Example
934/// ```
935/// use mockforge_core::validation::validate_url_safe;
936///
937/// assert!(validate_url_safe("https://example.com").is_ok());
938/// assert!(validate_url_safe("http://localhost:8080").is_err());
939/// assert!(validate_url_safe("http://169.254.169.254/metadata").is_err());
940/// ```
941pub fn validate_url_safe(url: &str) -> Result<String> {
942    // Parse URL to extract host
943    let url_lower = url.to_lowercase();
944
945    // Block localhost variants
946    let localhost_patterns = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"];
947    for pattern in localhost_patterns.iter() {
948        if url_lower.contains(pattern) {
949            return Err(Error::validation(
950                "URLs pointing to localhost are not allowed".to_string(),
951            ));
952        }
953    }
954
955    // Block private IP ranges (rough check)
956    let private_ranges = [
957        "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.",
958        "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.",
959        "172.31.", "192.168.",
960    ];
961    for range in private_ranges.iter() {
962        if url_lower.contains(range) {
963            return Err(Error::validation(format!(
964                "URLs pointing to private IP range '{}' are not allowed",
965                range
966            )));
967        }
968    }
969
970    // Block link-local addresses (AWS/cloud metadata endpoints)
971    if url_lower.contains("169.254.") {
972        return Err(Error::validation(
973            "URLs pointing to link-local addresses (169.254.x) are not allowed".to_string(),
974        ));
975    }
976
977    // Block common cloud metadata endpoints
978    let metadata_endpoints = [
979        "metadata.google.internal",
980        "169.254.169.254", // AWS, Azure, GCP
981        "fd00:ec2::254",   // AWS IPv6
982    ];
983    for endpoint in metadata_endpoints.iter() {
984        if url_lower.contains(endpoint) {
985            return Err(Error::validation(format!(
986                "URLs pointing to cloud metadata endpoint '{}' are not allowed",
987                endpoint
988            )));
989        }
990    }
991
992    Ok(url.to_string())
993}
994
995/// Sanitize header values to prevent header injection attacks
996///
997/// This function removes or escapes newline characters that could be used
998/// to inject additional HTTP headers.
999///
1000/// # Example
1001/// ```
1002/// use mockforge_core::validation::sanitize_header_value;
1003///
1004/// let malicious = "value\r\nX-Evil-Header: injected";
1005/// let safe = sanitize_header_value(malicious);
1006/// assert!(!safe.contains('\r'));
1007/// assert!(!safe.contains('\n'));
1008/// ```
1009pub fn sanitize_header_value(input: &str) -> String {
1010    // Remove CR and LF characters to prevent header injection
1011    input.replace(['\r', '\n'], "").trim().to_string()
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016    use super::*;
1017
1018    #[test]
1019    fn test_validation_result_success() {
1020        let result = ValidationResult::success();
1021        assert!(result.valid);
1022        assert!(result.errors.is_empty());
1023        assert!(result.warnings.is_empty());
1024    }
1025
1026    #[test]
1027    fn test_validation_result_failure() {
1028        let errors = vec!["error1".to_string(), "error2".to_string()];
1029        let result = ValidationResult::failure(errors.clone());
1030        assert!(!result.valid);
1031        assert_eq!(result.errors, errors);
1032        assert!(result.warnings.is_empty());
1033    }
1034
1035    #[test]
1036    fn test_validation_result_with_warning() {
1037        let result = ValidationResult::success()
1038            .with_warning("warning1".to_string())
1039            .with_warning("warning2".to_string());
1040        assert!(result.valid);
1041        assert_eq!(result.warnings.len(), 2);
1042    }
1043
1044    #[test]
1045    fn test_validator_from_json_schema() {
1046        let schema = json!({
1047            "type": "object",
1048            "properties": {
1049                "name": {"type": "string"}
1050            }
1051        });
1052
1053        let validator = Validator::from_json_schema(&schema);
1054        assert!(validator.is_ok());
1055        assert!(validator.unwrap().is_implemented());
1056    }
1057
1058    #[test]
1059    fn test_validator_from_json_schema_invalid() {
1060        let schema = json!({
1061            "type": "invalid_type"
1062        });
1063
1064        // Invalid schema should fail to compile
1065        let validator = Validator::from_json_schema(&schema);
1066        assert!(validator.is_err());
1067    }
1068
1069    #[test]
1070    fn test_validator_validate_json_schema_success() {
1071        let schema = json!({
1072            "type": "object",
1073            "properties": {
1074                "name": {"type": "string"}
1075            }
1076        });
1077
1078        let validator = Validator::from_json_schema(&schema).unwrap();
1079        let data = json!({"name": "test"});
1080
1081        assert!(validator.validate(&data).is_ok());
1082    }
1083
1084    #[test]
1085    fn test_validator_validate_json_schema_failure() {
1086        let schema = json!({
1087            "type": "object",
1088            "properties": {
1089                "name": {"type": "string"}
1090            }
1091        });
1092
1093        let validator = Validator::from_json_schema(&schema).unwrap();
1094        let data = json!({"name": 123});
1095
1096        assert!(validator.validate(&data).is_err());
1097    }
1098
1099    #[test]
1100    fn test_validator_from_openapi() {
1101        let spec = json!({
1102            "openapi": "3.0.0",
1103            "info": {"title": "Test", "version": "1.0.0"},
1104            "paths": {}
1105        });
1106
1107        let validator = Validator::from_openapi(&spec);
1108        assert!(validator.is_ok());
1109    }
1110
1111    #[test]
1112    fn test_validator_from_openapi_unsupported_version() {
1113        let spec = json!({
1114            "openapi": "2.0.0",
1115            "info": {"title": "Test", "version": "1.0.0"},
1116            "paths": {}
1117        });
1118
1119        let validator = Validator::from_openapi(&spec);
1120        assert!(validator.is_err());
1121    }
1122
1123    #[test]
1124    fn test_validator_validate_openapi() {
1125        let spec = json!({
1126            "openapi": "3.0.0",
1127            "info": {"title": "Test", "version": "1.0.0"},
1128            "paths": {}
1129        });
1130
1131        let validator = Validator::from_openapi(&spec).unwrap();
1132        let data = json!({"key": "value"});
1133
1134        assert!(validator.validate(&data).is_ok());
1135    }
1136
1137    #[test]
1138    fn test_validator_validate_openapi_non_object() {
1139        let spec = json!({
1140            "openapi": "3.0.0",
1141            "info": {"title": "Test", "version": "1.0.0"},
1142            "paths": {}
1143        });
1144
1145        let validator = Validator::from_openapi(&spec).unwrap();
1146        let data = json!("string");
1147
1148        assert!(validator.validate(&data).is_err());
1149    }
1150
1151    #[test]
1152    fn test_validate_json_schema_function() {
1153        let schema = json!({
1154            "type": "object",
1155            "properties": {
1156                "age": {"type": "number"}
1157            }
1158        });
1159
1160        let data = json!({"age": 25});
1161        let result = validate_json_schema(&data, &schema);
1162        assert!(result.valid);
1163
1164        let data = json!({"age": "25"});
1165        let result = validate_json_schema(&data, &schema);
1166        assert!(!result.valid);
1167    }
1168
1169    #[test]
1170    fn test_validate_openapi_function() {
1171        let spec = json!({
1172            "openapi": "3.0.0",
1173            "info": {"title": "Test", "version": "1.0.0"},
1174            "paths": {}
1175        });
1176
1177        let data = json!({"test": "value"});
1178        let result = validate_openapi(&data, &spec);
1179        assert!(result.valid);
1180    }
1181
1182    #[test]
1183    fn test_validate_openapi_missing_fields() {
1184        let spec = json!({
1185            "openapi": "3.0.0"
1186        });
1187
1188        let data = json!({});
1189        let result = validate_openapi(&data, &spec);
1190        assert!(!result.valid);
1191        assert!(!result.errors.is_empty());
1192    }
1193
1194    #[test]
1195    fn test_validate_number_constraints_multiple_of() {
1196        let schema = json!({
1197            "type": "number",
1198            "multipleOf": 5.0
1199        });
1200
1201        let validator = Validator::from_json_schema(&schema).unwrap();
1202
1203        let data = json!(10);
1204        assert!(validator.validate(&data).is_ok());
1205
1206        let data = json!(11);
1207        // JSON Schema validator may handle this differently
1208        // so we just test that it doesn't panic
1209        let _ = validator.validate(&data);
1210    }
1211
1212    #[test]
1213    fn test_validate_array_constraints_min_items() {
1214        let schema = json!({
1215            "type": "array",
1216            "minItems": 2
1217        });
1218
1219        let validator = Validator::from_json_schema(&schema).unwrap();
1220
1221        let data = json!([1, 2]);
1222        assert!(validator.validate(&data).is_ok());
1223
1224        let data = json!([1]);
1225        assert!(validator.validate(&data).is_err());
1226    }
1227
1228    #[test]
1229    fn test_validate_array_constraints_max_items() {
1230        let schema = json!({
1231            "type": "array",
1232            "maxItems": 2
1233        });
1234
1235        let validator = Validator::from_json_schema(&schema).unwrap();
1236
1237        let data = json!([1]);
1238        assert!(validator.validate(&data).is_ok());
1239
1240        let data = json!([1, 2, 3]);
1241        assert!(validator.validate(&data).is_err());
1242    }
1243
1244    #[test]
1245    fn test_validate_array_unique_items() {
1246        let schema = json!({
1247            "type": "array",
1248            "uniqueItems": true
1249        });
1250
1251        let validator = Validator::from_json_schema(&schema).unwrap();
1252
1253        let data = json!([1, 2, 3]);
1254        assert!(validator.validate(&data).is_ok());
1255
1256        let data = json!([1, 2, 2]);
1257        assert!(validator.validate(&data).is_err());
1258    }
1259
1260    #[test]
1261    fn test_validate_object_required_properties() {
1262        let schema = json!({
1263            "type": "object",
1264            "required": ["name", "age"]
1265        });
1266
1267        let validator = Validator::from_json_schema(&schema).unwrap();
1268
1269        let data = json!({"name": "test", "age": 25});
1270        assert!(validator.validate(&data).is_ok());
1271
1272        let data = json!({"name": "test"});
1273        assert!(validator.validate(&data).is_err());
1274    }
1275
1276    #[test]
1277    fn test_validate_content_encoding_base64() {
1278        let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1279
1280        // Valid base64
1281        let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
1282        assert!(result.is_ok());
1283
1284        // Invalid base64
1285        let result = validator.validate_content_encoding(Some("not-base64!@#"), "base64", "test");
1286        assert!(result.is_err());
1287    }
1288
1289    #[test]
1290    fn test_validate_content_encoding_hex() {
1291        let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1292
1293        // Valid hex
1294        let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
1295        assert!(result.is_ok());
1296
1297        // Invalid hex
1298        let result = validator.validate_content_encoding(Some("xyz"), "hex", "test");
1299        assert!(result.is_err());
1300    }
1301
1302    #[test]
1303    fn test_has_unique_items() {
1304        let validator = Validator::from_json_schema(&json!({})).unwrap();
1305
1306        let arr = vec![json!(1), json!(2), json!(3)];
1307        assert!(validator.has_unique_items(&arr));
1308
1309        let arr = vec![json!(1), json!(2), json!(1)];
1310        assert!(!validator.has_unique_items(&arr));
1311    }
1312
1313    #[test]
1314    fn test_validate_protobuf() {
1315        let result = validate_protobuf(&[], &[]);
1316        assert!(!result.valid);
1317        assert!(!result.errors.is_empty());
1318    }
1319
1320    #[test]
1321    fn test_validate_protobuf_with_type() {
1322        let result = validate_protobuf_with_type(&[], &[], "TestMessage");
1323        assert!(!result.valid);
1324        assert!(!result.errors.is_empty());
1325    }
1326
1327    #[test]
1328    fn test_is_implemented() {
1329        let json_validator = Validator::from_json_schema(&json!({"type": "object"})).unwrap();
1330        assert!(json_validator.is_implemented());
1331
1332        let openapi_validator = Validator::from_openapi(&json!({
1333            "openapi": "3.0.0",
1334            "info": {"title": "Test", "version": "1.0.0"},
1335            "paths": {}
1336        }))
1337        .unwrap();
1338        assert!(openapi_validator.is_implemented());
1339    }
1340
1341    // ========================================================================
1342    // SANITIZATION TESTS
1343    // ========================================================================
1344
1345    #[test]
1346    fn test_sanitize_html() {
1347        // Basic XSS attempt
1348        assert_eq!(
1349            sanitize_html("<script>alert('xss')</script>"),
1350            "&lt;script&gt;alert(&#39;xss&#39;)&lt;&#x2F;script&gt;"
1351        );
1352
1353        // Image tag with onerror
1354        assert_eq!(
1355            sanitize_html("<img src=x onerror=\"alert(1)\">"),
1356            "&lt;img src=x onerror=&quot;alert(1)&quot;&gt;"
1357        );
1358
1359        // JavaScript protocol
1360        assert_eq!(
1361            sanitize_html("<a href=\"javascript:void(0)\">"),
1362            "&lt;a href=&quot;javascript:void(0)&quot;&gt;"
1363        );
1364
1365        // Ampersand should be escaped first
1366        assert_eq!(sanitize_html("&<>"), "&amp;&lt;&gt;");
1367
1368        // Mixed content
1369        assert_eq!(
1370            sanitize_html("Hello <b>World</b> & 'Friends'"),
1371            "Hello &lt;b&gt;World&lt;&#x2F;b&gt; &amp; &#39;Friends&#39;"
1372        );
1373    }
1374
1375    #[test]
1376    fn test_validate_safe_path() {
1377        // Valid paths
1378        assert!(validate_safe_path("data/file.txt").is_ok());
1379        assert!(validate_safe_path("subdir/file.json").is_ok());
1380        assert!(validate_safe_path("file.txt").is_ok());
1381
1382        // Path traversal attempts
1383        assert!(validate_safe_path("../etc/passwd").is_err());
1384        assert!(validate_safe_path("dir/../../../etc/passwd").is_err());
1385        assert!(validate_safe_path("./../../secret").is_err());
1386
1387        // Home directory expansion
1388        assert!(validate_safe_path("~/secret").is_err());
1389        assert!(validate_safe_path("dir/~/file").is_err());
1390
1391        // Absolute paths
1392        assert!(validate_safe_path("/etc/passwd").is_err());
1393        assert!(validate_safe_path("/var/log/app.log").is_err());
1394
1395        // Windows drive letters
1396        assert!(validate_safe_path("C:\\Windows\\System32").is_err());
1397        assert!(validate_safe_path("D:\\data\\file.txt").is_err());
1398
1399        // UNC paths
1400        assert!(validate_safe_path("\\\\server\\share").is_err());
1401        assert!(validate_safe_path("//server/share").is_err());
1402
1403        // Null bytes
1404        assert!(validate_safe_path("file\0.txt").is_err());
1405
1406        // Empty segments
1407        assert!(validate_safe_path("dir//file.txt").is_err());
1408
1409        // Path normalization (backslash to forward slash)
1410        let result = validate_safe_path("dir\\subdir\\file.txt").unwrap();
1411        assert_eq!(result, "dir/subdir/file.txt");
1412    }
1413
1414    #[test]
1415    fn test_sanitize_sql() {
1416        // Basic SQL injection
1417        assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
1418
1419        // Multiple quotes
1420        assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
1421
1422        // No quotes
1423        assert_eq!(sanitize_sql("admin"), "admin");
1424
1425        // Single quote
1426        assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
1427    }
1428
1429    #[test]
1430    fn test_validate_command_arg() {
1431        // Safe arguments
1432        assert!(validate_command_arg("safe_filename.txt").is_ok());
1433        assert!(validate_command_arg("file-123.log").is_ok());
1434        assert!(validate_command_arg("data.json").is_ok());
1435
1436        // Command injection attempts - pipes
1437        assert!(validate_command_arg("file | cat /etc/passwd").is_err());
1438        assert!(validate_command_arg("file || echo pwned").is_err());
1439
1440        // Command separators
1441        assert!(validate_command_arg("file; rm -rf /").is_err());
1442        assert!(validate_command_arg("file & background").is_err());
1443        assert!(validate_command_arg("file && next").is_err());
1444
1445        // Redirection
1446        assert!(validate_command_arg("file > /dev/null").is_err());
1447        assert!(validate_command_arg("file < input.txt").is_err());
1448        assert!(validate_command_arg("file >> log.txt").is_err());
1449
1450        // Command substitution
1451        assert!(validate_command_arg("file `whoami`").is_err());
1452        assert!(validate_command_arg("file $(whoami)").is_err());
1453
1454        // Wildcards
1455        assert!(validate_command_arg("file*.txt").is_err());
1456        assert!(validate_command_arg("file?.log").is_err());
1457
1458        // Brackets
1459        assert!(validate_command_arg("file[0-9]").is_err());
1460        assert!(validate_command_arg("file{1,2}").is_err());
1461
1462        // Null byte
1463        assert!(validate_command_arg("file\0.txt").is_err());
1464
1465        // Newlines
1466        assert!(validate_command_arg("file\nrm -rf /").is_err());
1467        assert!(validate_command_arg("file\rcommand").is_err());
1468
1469        // Other dangerous chars
1470        assert!(validate_command_arg("file~").is_err());
1471        assert!(validate_command_arg("file!").is_err());
1472    }
1473
1474    #[test]
1475    fn test_sanitize_json_string() {
1476        // Quote injection
1477        assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
1478
1479        // Backslash escape
1480        assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
1481
1482        // Control characters
1483        assert_eq!(sanitize_json_string("line1\nline2"), r#"line1\nline2"#);
1484        assert_eq!(sanitize_json_string("tab\there"), r#"tab\there"#);
1485        assert_eq!(sanitize_json_string("carriage\rreturn"), r#"carriage\rreturn"#);
1486
1487        // Combined
1488        assert_eq!(
1489            sanitize_json_string("Test\"value\"\nNext\\line"),
1490            r#"Test\"value\"\nNext\\line"#
1491        );
1492    }
1493
1494    #[test]
1495    fn test_validate_url_safe() {
1496        // Safe URLs
1497        assert!(validate_url_safe("https://example.com").is_ok());
1498        assert!(validate_url_safe("http://api.example.com/data").is_ok());
1499        assert!(validate_url_safe("https://subdomain.example.org:8080/path").is_ok());
1500
1501        // Localhost variants
1502        assert!(validate_url_safe("http://localhost:8080").is_err());
1503        assert!(validate_url_safe("http://127.0.0.1").is_err());
1504        assert!(validate_url_safe("http://[::1]:8080").is_err());
1505        assert!(validate_url_safe("http://0.0.0.0").is_err());
1506
1507        // Private IP ranges
1508        assert!(validate_url_safe("http://10.0.0.1").is_err());
1509        assert!(validate_url_safe("http://192.168.1.1").is_err());
1510        assert!(validate_url_safe("http://172.16.0.1").is_err());
1511        assert!(validate_url_safe("http://172.31.255.255").is_err());
1512
1513        // Link-local (AWS metadata)
1514        assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
1515
1516        // Cloud metadata endpoints
1517        assert!(validate_url_safe("http://metadata.google.internal").is_err());
1518        assert!(validate_url_safe("http://169.254.169.254").is_err());
1519
1520        // Case insensitive
1521        assert!(validate_url_safe("HTTP://LOCALHOST:8080").is_err());
1522        assert!(validate_url_safe("http://LocalHost").is_err());
1523    }
1524
1525    #[test]
1526    fn test_sanitize_header_value() {
1527        // Header injection attempt
1528        let malicious = "value\r\nX-Evil-Header: injected";
1529        let safe = sanitize_header_value(malicious);
1530        assert!(!safe.contains('\r'));
1531        assert!(!safe.contains('\n'));
1532        assert_eq!(safe, "valueX-Evil-Header: injected");
1533
1534        // CRLF injection
1535        let malicious = "session123\r\nSet-Cookie: admin=true";
1536        let safe = sanitize_header_value(malicious);
1537        assert_eq!(safe, "session123Set-Cookie: admin=true");
1538
1539        // Whitespace trimming
1540        assert_eq!(sanitize_header_value("  value  "), "value");
1541
1542        // Multiple newlines
1543        let malicious = "val\nue\r\nhe\na\rder";
1544        let safe = sanitize_header_value(malicious);
1545        assert_eq!(safe, "valueheader");
1546
1547        // Clean value
1548        assert_eq!(sanitize_header_value("clean-value-123"), "clean-value-123");
1549    }
1550
1551    #[test]
1552    fn test_sanitize_html_empty_and_whitespace() {
1553        assert_eq!(sanitize_html(""), "");
1554        assert_eq!(sanitize_html("   "), "   ");
1555    }
1556
1557    #[test]
1558    fn test_validate_safe_path_edge_cases() {
1559        // Single dot (current directory) - should be allowed
1560        assert!(validate_safe_path(".").is_ok());
1561
1562        // Just a filename
1563        assert!(validate_safe_path("README.md").is_ok());
1564
1565        // Deep nested path
1566        assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
1567
1568        // Multiple dots in filename (not traversal)
1569        assert!(validate_safe_path("file.test.txt").is_ok());
1570
1571        // But two consecutive dots are blocked
1572        assert!(validate_safe_path("..").is_err());
1573        assert!(validate_safe_path("dir/..").is_err());
1574    }
1575
1576    #[test]
1577    fn test_sanitize_sql_edge_cases() {
1578        // Empty string
1579        assert_eq!(sanitize_sql(""), "");
1580
1581        // Already escaped
1582        assert_eq!(sanitize_sql("''"), "''''");
1583
1584        // Multiple consecutive quotes
1585        assert_eq!(sanitize_sql("'''"), "''''''");
1586    }
1587
1588    #[test]
1589    fn test_validate_command_arg_edge_cases() {
1590        // Empty string
1591        assert!(validate_command_arg("").is_ok());
1592
1593        // Alphanumeric with dash and underscore
1594        assert!(validate_command_arg("file_name-123").is_ok());
1595
1596        // Just numbers
1597        assert!(validate_command_arg("12345").is_ok());
1598    }
1599}