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