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    // For now, return an error as protobuf validation is not yet fully implemented
626    // This would require proper protobuf descriptor handling
627    ValidationResult::failure(vec!["Protobuf validation is not yet fully implemented".to_string()])
628}
629
630/// Validate protobuf data against a specific message descriptor
631pub fn validate_protobuf_message(
632    data: &[u8],
633    message_descriptor: &prost_reflect::MessageDescriptor,
634) -> Result<()> {
635    // Try to decode the data as the given message type
636    match DynamicMessage::decode(message_descriptor.clone(), data) {
637        Ok(_) => Ok(()),
638        Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
639    }
640}
641
642/// Validate protobuf data with explicit message type name
643pub fn validate_protobuf_with_type(
644    _data: &[u8],
645    _descriptor_data: &[u8],
646    _message_type_name: &str,
647) -> ValidationResult {
648    // For now, return an error as protobuf validation is not fully implemented
649    // This would require proper protobuf descriptor handling
650    ValidationResult::failure(vec!["Protobuf validation is not yet fully implemented".to_string()])
651}
652
653/// Validate OpenAPI security requirements
654pub fn validate_openapi_security(
655    spec: &OpenApiSpec,
656    security_requirements: &[OpenApiSecurityRequirement],
657    auth_header: Option<&str>,
658    api_key: Option<&str>,
659) -> ValidationResult {
660    match spec.validate_security_requirements(security_requirements, auth_header, api_key) {
661        Ok(_) => ValidationResult::success(),
662        Err(e) => ValidationResult::failure(vec![format!("Security validation failed: {}", e)]),
663    }
664}
665
666/// Validate security for a specific OpenAPI operation
667pub fn validate_openapi_operation_security(
668    spec: &OpenApiSpec,
669    path: &str,
670    method: &str,
671    auth_header: Option<&str>,
672    api_key: Option<&str>,
673) -> ValidationResult {
674    // Get operations for this path
675    let operations = spec.operations_for_path(path);
676
677    // Find the specific operation
678    let operation = operations
679        .iter()
680        .find(|(op_method, _)| op_method.to_uppercase() == method.to_uppercase());
681
682    let operation = match operation {
683        Some((_, op)) => op,
684        None => {
685            return ValidationResult::failure(vec![format!(
686                "Operation not found: {} {}",
687                method, path
688            )])
689        }
690    };
691
692    // Convert operation to OpenApiOperation for security validation
693    let openapi_operation =
694        OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
695
696    // Check operation-specific security first
697    if let Some(ref security_reqs) = openapi_operation.security {
698        if !security_reqs.is_empty() {
699            return validate_openapi_security(spec, security_reqs, auth_header, api_key);
700        }
701    }
702
703    // Fall back to global security requirements
704    let global_security = spec.get_global_security_requirements();
705    if !global_security.is_empty() {
706        return validate_openapi_security(spec, &global_security, auth_header, api_key);
707    }
708
709    // No security requirements
710    ValidationResult::success()
711}
712
713// ============================================================================
714// INPUT SANITIZATION
715// ============================================================================
716
717/// Sanitize HTML to prevent XSS attacks
718///
719/// This function escapes HTML special characters to prevent script injection.
720/// Use this for any user-provided content that will be displayed in HTML contexts.
721///
722/// # Example
723/// ```
724/// use mockforge_core::validation::sanitize_html;
725///
726/// let malicious = "<script>alert('xss')</script>";
727/// let safe = sanitize_html(malicious);
728/// assert_eq!(safe, "&lt;script&gt;alert(&#39;xss&#39;)&lt;&#x2F;script&gt;");
729/// ```
730pub fn sanitize_html(input: &str) -> String {
731    input
732        .replace('&', "&amp;")
733        .replace('<', "&lt;")
734        .replace('>', "&gt;")
735        .replace('"', "&quot;")
736        .replace('\'', "&#39;")
737        .replace('/', "&#x2F;")
738}
739
740/// Validate and sanitize file paths to prevent path traversal attacks
741///
742/// This function checks for common path traversal patterns and returns an error
743/// if any are detected. It also normalizes the path to prevent bypass attempts.
744///
745/// # Security Concerns
746/// - Blocks `..` (parent directory)
747/// - Blocks `~` (home directory expansion)
748/// - Blocks absolute paths (starting with `/` or drive letters on Windows)
749/// - Blocks null bytes
750///
751/// # Example
752/// ```
753/// use mockforge_core::validation::validate_safe_path;
754///
755/// assert!(validate_safe_path("data/file.txt").is_ok());
756/// assert!(validate_safe_path("../etc/passwd").is_err());
757/// assert!(validate_safe_path("/etc/passwd").is_err());
758/// ```
759pub fn validate_safe_path(path: &str) -> Result<String> {
760    // Check for null bytes
761    if path.contains('\0') {
762        return Err(Error::validation("Path contains null bytes".to_string()));
763    }
764
765    // Check for path traversal attempts
766    if path.contains("..") {
767        return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
768    }
769
770    // Check for home directory expansion
771    if path.contains('~') {
772        return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
773    }
774
775    // Check for absolute paths (Unix)
776    if path.starts_with('/') {
777        return Err(Error::validation("Absolute paths not allowed".to_string()));
778    }
779
780    // Check for absolute paths (Windows drive letters)
781    if path.len() >= 2 && path.chars().nth(1) == Some(':') {
782        return Err(Error::validation("Absolute paths with drive letters not allowed".to_string()));
783    }
784
785    // Check for UNC paths (Windows network paths)
786    if path.starts_with("\\\\") || path.starts_with("//") {
787        return Err(Error::validation("UNC paths not allowed".to_string()));
788    }
789
790    // Normalize path separators to forward slashes
791    let normalized = path.replace('\\', "/");
792
793    // Additional check: ensure no empty segments (e.g., "foo//bar")
794    if normalized.contains("//") {
795        return Err(Error::validation("Path contains empty segments".to_string()));
796    }
797
798    Ok(normalized)
799}
800
801/// Sanitize SQL input to prevent SQL injection
802///
803/// This function escapes SQL special characters. However, **parameterized queries
804/// should always be preferred** over manual sanitization.
805///
806/// # Warning
807/// This is a last-resort defense. Always use parameterized queries when possible.
808///
809/// # Example
810/// ```
811/// use mockforge_core::validation::sanitize_sql;
812///
813/// let input = "admin' OR '1'='1";
814/// let safe = sanitize_sql(input);
815/// assert_eq!(safe, "admin'' OR ''1''=''1");
816/// ```
817pub fn sanitize_sql(input: &str) -> String {
818    // Escape single quotes by doubling them (SQL standard)
819    input.replace('\'', "''")
820}
821
822/// Validate command arguments to prevent command injection
823///
824/// This function checks for shell metacharacters and returns an error if any
825/// are detected. Use this when building shell commands from user input.
826///
827/// # Security Concerns
828/// Blocks the following shell metacharacters:
829/// - Pipes: `|`, `||`
830/// - Command separators: `;`, `&`, `&&`
831/// - Redirection: `<`, `>`, `>>`
832/// - Command substitution: `` ` ``, `$(`, `)`
833/// - Wildcards: `*`, `?`
834/// - Null byte: `\0`
835///
836/// # Example
837/// ```
838/// use mockforge_core::validation::validate_command_arg;
839///
840/// assert!(validate_command_arg("safe_filename.txt").is_ok());
841/// assert!(validate_command_arg("file; rm -rf /").is_err());
842/// assert!(validate_command_arg("file | cat /etc/passwd").is_err());
843/// ```
844pub fn validate_command_arg(arg: &str) -> Result<String> {
845    // List of dangerous shell metacharacters
846    let dangerous_chars = [
847        '|', ';', '&', '<', '>', '`', '$', '(', ')', '*', '?', '[', ']', '{', '}', '~', '!', '\n',
848        '\r', '\0',
849    ];
850
851    for ch in dangerous_chars.iter() {
852        if arg.contains(*ch) {
853            return Err(Error::validation(format!(
854                "Command argument contains dangerous character: '{}'",
855                ch
856            )));
857        }
858    }
859
860    // Check for command substitution patterns
861    if arg.contains("$(") {
862        return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
863    }
864
865    Ok(arg.to_string())
866}
867
868/// Sanitize JSON string values to prevent JSON injection
869///
870/// This function escapes special characters in JSON string values to prevent
871/// injection attacks when building JSON dynamically.
872///
873/// # Example
874/// ```
875/// use mockforge_core::validation::sanitize_json_string;
876///
877/// let input = r#"test","admin":true,"#;
878/// let safe = sanitize_json_string(input);
879/// assert!(safe.contains(r#"\""#));
880/// ```
881pub fn sanitize_json_string(input: &str) -> String {
882    input
883        .replace('\\', "\\\\") // Backslash must be first
884        .replace('"', "\\\"")
885        .replace('\n', "\\n")
886        .replace('\r', "\\r")
887        .replace('\t', "\\t")
888}
889
890/// Validate URL to prevent SSRF (Server-Side Request Forgery) attacks
891///
892/// This function checks URLs for private IP ranges, localhost, and metadata endpoints
893/// that could be exploited in SSRF attacks.
894///
895/// # Security Concerns
896/// - Blocks localhost (127.0.0.1, ::1, localhost)
897/// - Blocks private IP ranges (10.x, 172.16-31.x, 192.168.x)
898/// - Blocks link-local addresses (169.254.x)
899/// - Blocks cloud metadata endpoints
900///
901/// # Example
902/// ```
903/// use mockforge_core::validation::validate_url_safe;
904///
905/// assert!(validate_url_safe("https://example.com").is_ok());
906/// assert!(validate_url_safe("http://localhost:8080").is_err());
907/// assert!(validate_url_safe("http://169.254.169.254/metadata").is_err());
908/// ```
909pub fn validate_url_safe(url: &str) -> Result<String> {
910    // Parse URL to extract host
911    let url_lower = url.to_lowercase();
912
913    // Block localhost variants
914    let localhost_patterns = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"];
915    for pattern in localhost_patterns.iter() {
916        if url_lower.contains(pattern) {
917            return Err(Error::validation(
918                "URLs pointing to localhost are not allowed".to_string(),
919            ));
920        }
921    }
922
923    // Block private IP ranges (rough check)
924    let private_ranges = [
925        "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.",
926        "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.",
927        "172.31.", "192.168.",
928    ];
929    for range in private_ranges.iter() {
930        if url_lower.contains(range) {
931            return Err(Error::validation(format!(
932                "URLs pointing to private IP range '{}' are not allowed",
933                range
934            )));
935        }
936    }
937
938    // Block link-local addresses (AWS/cloud metadata endpoints)
939    if url_lower.contains("169.254.") {
940        return Err(Error::validation(
941            "URLs pointing to link-local addresses (169.254.x) are not allowed".to_string(),
942        ));
943    }
944
945    // Block common cloud metadata endpoints
946    let metadata_endpoints = [
947        "metadata.google.internal",
948        "169.254.169.254", // AWS, Azure, GCP
949        "fd00:ec2::254",   // AWS IPv6
950    ];
951    for endpoint in metadata_endpoints.iter() {
952        if url_lower.contains(endpoint) {
953            return Err(Error::validation(format!(
954                "URLs pointing to cloud metadata endpoint '{}' are not allowed",
955                endpoint
956            )));
957        }
958    }
959
960    Ok(url.to_string())
961}
962
963/// Sanitize header values to prevent header injection attacks
964///
965/// This function removes or escapes newline characters that could be used
966/// to inject additional HTTP headers.
967///
968/// # Example
969/// ```
970/// use mockforge_core::validation::sanitize_header_value;
971///
972/// let malicious = "value\r\nX-Evil-Header: injected";
973/// let safe = sanitize_header_value(malicious);
974/// assert!(!safe.contains('\r'));
975/// assert!(!safe.contains('\n'));
976/// ```
977pub fn sanitize_header_value(input: &str) -> String {
978    // Remove CR and LF characters to prevent header injection
979    input.replace(['\r', '\n'], "").trim().to_string()
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985
986    #[test]
987    fn test_validation_result_success() {
988        let result = ValidationResult::success();
989        assert!(result.valid);
990        assert!(result.errors.is_empty());
991        assert!(result.warnings.is_empty());
992    }
993
994    #[test]
995    fn test_validation_result_failure() {
996        let errors = vec!["error1".to_string(), "error2".to_string()];
997        let result = ValidationResult::failure(errors.clone());
998        assert!(!result.valid);
999        assert_eq!(result.errors, errors);
1000        assert!(result.warnings.is_empty());
1001    }
1002
1003    #[test]
1004    fn test_validation_result_with_warning() {
1005        let result = ValidationResult::success()
1006            .with_warning("warning1".to_string())
1007            .with_warning("warning2".to_string());
1008        assert!(result.valid);
1009        assert_eq!(result.warnings.len(), 2);
1010    }
1011
1012    #[test]
1013    fn test_validator_from_json_schema() {
1014        let schema = json!({
1015            "type": "object",
1016            "properties": {
1017                "name": {"type": "string"}
1018            }
1019        });
1020
1021        let validator = Validator::from_json_schema(&schema);
1022        assert!(validator.is_ok());
1023        assert!(validator.unwrap().is_implemented());
1024    }
1025
1026    #[test]
1027    fn test_validator_from_json_schema_invalid() {
1028        let schema = json!({
1029            "type": "invalid_type"
1030        });
1031
1032        // Invalid schema should fail to compile
1033        let validator = Validator::from_json_schema(&schema);
1034        assert!(validator.is_err());
1035    }
1036
1037    #[test]
1038    fn test_validator_validate_json_schema_success() {
1039        let schema = json!({
1040            "type": "object",
1041            "properties": {
1042                "name": {"type": "string"}
1043            }
1044        });
1045
1046        let validator = Validator::from_json_schema(&schema).unwrap();
1047        let data = json!({"name": "test"});
1048
1049        assert!(validator.validate(&data).is_ok());
1050    }
1051
1052    #[test]
1053    fn test_validator_validate_json_schema_failure() {
1054        let schema = json!({
1055            "type": "object",
1056            "properties": {
1057                "name": {"type": "string"}
1058            }
1059        });
1060
1061        let validator = Validator::from_json_schema(&schema).unwrap();
1062        let data = json!({"name": 123});
1063
1064        assert!(validator.validate(&data).is_err());
1065    }
1066
1067    #[test]
1068    fn test_validator_from_openapi() {
1069        let spec = json!({
1070            "openapi": "3.0.0",
1071            "info": {"title": "Test", "version": "1.0.0"},
1072            "paths": {}
1073        });
1074
1075        let validator = Validator::from_openapi(&spec);
1076        assert!(validator.is_ok());
1077    }
1078
1079    #[test]
1080    fn test_validator_from_openapi_unsupported_version() {
1081        let spec = json!({
1082            "openapi": "2.0.0",
1083            "info": {"title": "Test", "version": "1.0.0"},
1084            "paths": {}
1085        });
1086
1087        let validator = Validator::from_openapi(&spec);
1088        assert!(validator.is_err());
1089    }
1090
1091    #[test]
1092    fn test_validator_validate_openapi() {
1093        let spec = json!({
1094            "openapi": "3.0.0",
1095            "info": {"title": "Test", "version": "1.0.0"},
1096            "paths": {}
1097        });
1098
1099        let validator = Validator::from_openapi(&spec).unwrap();
1100        let data = json!({"key": "value"});
1101
1102        assert!(validator.validate(&data).is_ok());
1103    }
1104
1105    #[test]
1106    fn test_validator_validate_openapi_non_object() {
1107        let spec = json!({
1108            "openapi": "3.0.0",
1109            "info": {"title": "Test", "version": "1.0.0"},
1110            "paths": {}
1111        });
1112
1113        let validator = Validator::from_openapi(&spec).unwrap();
1114        let data = json!("string");
1115
1116        assert!(validator.validate(&data).is_err());
1117    }
1118
1119    #[test]
1120    fn test_validate_json_schema_function() {
1121        let schema = json!({
1122            "type": "object",
1123            "properties": {
1124                "age": {"type": "number"}
1125            }
1126        });
1127
1128        let data = json!({"age": 25});
1129        let result = validate_json_schema(&data, &schema);
1130        assert!(result.valid);
1131
1132        let data = json!({"age": "25"});
1133        let result = validate_json_schema(&data, &schema);
1134        assert!(!result.valid);
1135    }
1136
1137    #[test]
1138    fn test_validate_openapi_function() {
1139        let spec = json!({
1140            "openapi": "3.0.0",
1141            "info": {"title": "Test", "version": "1.0.0"},
1142            "paths": {}
1143        });
1144
1145        let data = json!({"test": "value"});
1146        let result = validate_openapi(&data, &spec);
1147        assert!(result.valid);
1148    }
1149
1150    #[test]
1151    fn test_validate_openapi_missing_fields() {
1152        let spec = json!({
1153            "openapi": "3.0.0"
1154        });
1155
1156        let data = json!({});
1157        let result = validate_openapi(&data, &spec);
1158        assert!(!result.valid);
1159        assert!(!result.errors.is_empty());
1160    }
1161
1162    #[test]
1163    fn test_validate_number_constraints_multiple_of() {
1164        let schema = json!({
1165            "type": "number",
1166            "multipleOf": 5.0
1167        });
1168
1169        let validator = Validator::from_json_schema(&schema).unwrap();
1170
1171        let data = json!(10);
1172        assert!(validator.validate(&data).is_ok());
1173
1174        let data = json!(11);
1175        // JSON Schema validator may handle this differently
1176        // so we just test that it doesn't panic
1177        let _ = validator.validate(&data);
1178    }
1179
1180    #[test]
1181    fn test_validate_array_constraints_min_items() {
1182        let schema = json!({
1183            "type": "array",
1184            "minItems": 2
1185        });
1186
1187        let validator = Validator::from_json_schema(&schema).unwrap();
1188
1189        let data = json!([1, 2]);
1190        assert!(validator.validate(&data).is_ok());
1191
1192        let data = json!([1]);
1193        assert!(validator.validate(&data).is_err());
1194    }
1195
1196    #[test]
1197    fn test_validate_array_constraints_max_items() {
1198        let schema = json!({
1199            "type": "array",
1200            "maxItems": 2
1201        });
1202
1203        let validator = Validator::from_json_schema(&schema).unwrap();
1204
1205        let data = json!([1]);
1206        assert!(validator.validate(&data).is_ok());
1207
1208        let data = json!([1, 2, 3]);
1209        assert!(validator.validate(&data).is_err());
1210    }
1211
1212    #[test]
1213    fn test_validate_array_unique_items() {
1214        let schema = json!({
1215            "type": "array",
1216            "uniqueItems": true
1217        });
1218
1219        let validator = Validator::from_json_schema(&schema).unwrap();
1220
1221        let data = json!([1, 2, 3]);
1222        assert!(validator.validate(&data).is_ok());
1223
1224        let data = json!([1, 2, 2]);
1225        assert!(validator.validate(&data).is_err());
1226    }
1227
1228    #[test]
1229    fn test_validate_object_required_properties() {
1230        let schema = json!({
1231            "type": "object",
1232            "required": ["name", "age"]
1233        });
1234
1235        let validator = Validator::from_json_schema(&schema).unwrap();
1236
1237        let data = json!({"name": "test", "age": 25});
1238        assert!(validator.validate(&data).is_ok());
1239
1240        let data = json!({"name": "test"});
1241        assert!(validator.validate(&data).is_err());
1242    }
1243
1244    #[test]
1245    fn test_validate_content_encoding_base64() {
1246        let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1247
1248        // Valid base64
1249        let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
1250        assert!(result.is_ok());
1251
1252        // Invalid base64
1253        let result = validator.validate_content_encoding(Some("not-base64!@#"), "base64", "test");
1254        assert!(result.is_err());
1255    }
1256
1257    #[test]
1258    fn test_validate_content_encoding_hex() {
1259        let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1260
1261        // Valid hex
1262        let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
1263        assert!(result.is_ok());
1264
1265        // Invalid hex
1266        let result = validator.validate_content_encoding(Some("xyz"), "hex", "test");
1267        assert!(result.is_err());
1268    }
1269
1270    #[test]
1271    fn test_has_unique_items() {
1272        let validator = Validator::from_json_schema(&json!({})).unwrap();
1273
1274        let arr = vec![json!(1), json!(2), json!(3)];
1275        assert!(validator.has_unique_items(&arr));
1276
1277        let arr = vec![json!(1), json!(2), json!(1)];
1278        assert!(!validator.has_unique_items(&arr));
1279    }
1280
1281    #[test]
1282    fn test_validate_protobuf() {
1283        let result = validate_protobuf(&[], &[]);
1284        assert!(!result.valid);
1285        assert!(result.errors[0].contains("not yet fully implemented"));
1286    }
1287
1288    #[test]
1289    fn test_validate_protobuf_with_type() {
1290        let result = validate_protobuf_with_type(&[], &[], "TestMessage");
1291        assert!(!result.valid);
1292        assert!(result.errors[0].contains("not yet fully implemented"));
1293    }
1294
1295    #[test]
1296    fn test_is_implemented() {
1297        let json_validator = Validator::from_json_schema(&json!({"type": "object"})).unwrap();
1298        assert!(json_validator.is_implemented());
1299
1300        let openapi_validator = Validator::from_openapi(&json!({
1301            "openapi": "3.0.0",
1302            "info": {"title": "Test", "version": "1.0.0"},
1303            "paths": {}
1304        }))
1305        .unwrap();
1306        assert!(openapi_validator.is_implemented());
1307    }
1308
1309    // ========================================================================
1310    // SANITIZATION TESTS
1311    // ========================================================================
1312
1313    #[test]
1314    fn test_sanitize_html() {
1315        // Basic XSS attempt
1316        assert_eq!(
1317            sanitize_html("<script>alert('xss')</script>"),
1318            "&lt;script&gt;alert(&#39;xss&#39;)&lt;&#x2F;script&gt;"
1319        );
1320
1321        // Image tag with onerror
1322        assert_eq!(
1323            sanitize_html("<img src=x onerror=\"alert(1)\">"),
1324            "&lt;img src=x onerror=&quot;alert(1)&quot;&gt;"
1325        );
1326
1327        // JavaScript protocol
1328        assert_eq!(
1329            sanitize_html("<a href=\"javascript:void(0)\">"),
1330            "&lt;a href=&quot;javascript:void(0)&quot;&gt;"
1331        );
1332
1333        // Ampersand should be escaped first
1334        assert_eq!(sanitize_html("&<>"), "&amp;&lt;&gt;");
1335
1336        // Mixed content
1337        assert_eq!(
1338            sanitize_html("Hello <b>World</b> & 'Friends'"),
1339            "Hello &lt;b&gt;World&lt;&#x2F;b&gt; &amp; &#39;Friends&#39;"
1340        );
1341    }
1342
1343    #[test]
1344    fn test_validate_safe_path() {
1345        // Valid paths
1346        assert!(validate_safe_path("data/file.txt").is_ok());
1347        assert!(validate_safe_path("subdir/file.json").is_ok());
1348        assert!(validate_safe_path("file.txt").is_ok());
1349
1350        // Path traversal attempts
1351        assert!(validate_safe_path("../etc/passwd").is_err());
1352        assert!(validate_safe_path("dir/../../../etc/passwd").is_err());
1353        assert!(validate_safe_path("./../../secret").is_err());
1354
1355        // Home directory expansion
1356        assert!(validate_safe_path("~/secret").is_err());
1357        assert!(validate_safe_path("dir/~/file").is_err());
1358
1359        // Absolute paths
1360        assert!(validate_safe_path("/etc/passwd").is_err());
1361        assert!(validate_safe_path("/var/log/app.log").is_err());
1362
1363        // Windows drive letters
1364        assert!(validate_safe_path("C:\\Windows\\System32").is_err());
1365        assert!(validate_safe_path("D:\\data\\file.txt").is_err());
1366
1367        // UNC paths
1368        assert!(validate_safe_path("\\\\server\\share").is_err());
1369        assert!(validate_safe_path("//server/share").is_err());
1370
1371        // Null bytes
1372        assert!(validate_safe_path("file\0.txt").is_err());
1373
1374        // Empty segments
1375        assert!(validate_safe_path("dir//file.txt").is_err());
1376
1377        // Path normalization (backslash to forward slash)
1378        let result = validate_safe_path("dir\\subdir\\file.txt").unwrap();
1379        assert_eq!(result, "dir/subdir/file.txt");
1380    }
1381
1382    #[test]
1383    fn test_sanitize_sql() {
1384        // Basic SQL injection
1385        assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
1386
1387        // Multiple quotes
1388        assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
1389
1390        // No quotes
1391        assert_eq!(sanitize_sql("admin"), "admin");
1392
1393        // Single quote
1394        assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
1395    }
1396
1397    #[test]
1398    fn test_validate_command_arg() {
1399        // Safe arguments
1400        assert!(validate_command_arg("safe_filename.txt").is_ok());
1401        assert!(validate_command_arg("file-123.log").is_ok());
1402        assert!(validate_command_arg("data.json").is_ok());
1403
1404        // Command injection attempts - pipes
1405        assert!(validate_command_arg("file | cat /etc/passwd").is_err());
1406        assert!(validate_command_arg("file || echo pwned").is_err());
1407
1408        // Command separators
1409        assert!(validate_command_arg("file; rm -rf /").is_err());
1410        assert!(validate_command_arg("file & background").is_err());
1411        assert!(validate_command_arg("file && next").is_err());
1412
1413        // Redirection
1414        assert!(validate_command_arg("file > /dev/null").is_err());
1415        assert!(validate_command_arg("file < input.txt").is_err());
1416        assert!(validate_command_arg("file >> log.txt").is_err());
1417
1418        // Command substitution
1419        assert!(validate_command_arg("file `whoami`").is_err());
1420        assert!(validate_command_arg("file $(whoami)").is_err());
1421
1422        // Wildcards
1423        assert!(validate_command_arg("file*.txt").is_err());
1424        assert!(validate_command_arg("file?.log").is_err());
1425
1426        // Brackets
1427        assert!(validate_command_arg("file[0-9]").is_err());
1428        assert!(validate_command_arg("file{1,2}").is_err());
1429
1430        // Null byte
1431        assert!(validate_command_arg("file\0.txt").is_err());
1432
1433        // Newlines
1434        assert!(validate_command_arg("file\nrm -rf /").is_err());
1435        assert!(validate_command_arg("file\rcommand").is_err());
1436
1437        // Other dangerous chars
1438        assert!(validate_command_arg("file~").is_err());
1439        assert!(validate_command_arg("file!").is_err());
1440    }
1441
1442    #[test]
1443    fn test_sanitize_json_string() {
1444        // Quote injection
1445        assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
1446
1447        // Backslash escape
1448        assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
1449
1450        // Control characters
1451        assert_eq!(sanitize_json_string("line1\nline2"), r#"line1\nline2"#);
1452        assert_eq!(sanitize_json_string("tab\there"), r#"tab\there"#);
1453        assert_eq!(sanitize_json_string("carriage\rreturn"), r#"carriage\rreturn"#);
1454
1455        // Combined
1456        assert_eq!(
1457            sanitize_json_string("Test\"value\"\nNext\\line"),
1458            r#"Test\"value\"\nNext\\line"#
1459        );
1460    }
1461
1462    #[test]
1463    fn test_validate_url_safe() {
1464        // Safe URLs
1465        assert!(validate_url_safe("https://example.com").is_ok());
1466        assert!(validate_url_safe("http://api.example.com/data").is_ok());
1467        assert!(validate_url_safe("https://subdomain.example.org:8080/path").is_ok());
1468
1469        // Localhost variants
1470        assert!(validate_url_safe("http://localhost:8080").is_err());
1471        assert!(validate_url_safe("http://127.0.0.1").is_err());
1472        assert!(validate_url_safe("http://[::1]:8080").is_err());
1473        assert!(validate_url_safe("http://0.0.0.0").is_err());
1474
1475        // Private IP ranges
1476        assert!(validate_url_safe("http://10.0.0.1").is_err());
1477        assert!(validate_url_safe("http://192.168.1.1").is_err());
1478        assert!(validate_url_safe("http://172.16.0.1").is_err());
1479        assert!(validate_url_safe("http://172.31.255.255").is_err());
1480
1481        // Link-local (AWS metadata)
1482        assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
1483
1484        // Cloud metadata endpoints
1485        assert!(validate_url_safe("http://metadata.google.internal").is_err());
1486        assert!(validate_url_safe("http://169.254.169.254").is_err());
1487
1488        // Case insensitive
1489        assert!(validate_url_safe("HTTP://LOCALHOST:8080").is_err());
1490        assert!(validate_url_safe("http://LocalHost").is_err());
1491    }
1492
1493    #[test]
1494    fn test_sanitize_header_value() {
1495        // Header injection attempt
1496        let malicious = "value\r\nX-Evil-Header: injected";
1497        let safe = sanitize_header_value(malicious);
1498        assert!(!safe.contains('\r'));
1499        assert!(!safe.contains('\n'));
1500        assert_eq!(safe, "valueX-Evil-Header: injected");
1501
1502        // CRLF injection
1503        let malicious = "session123\r\nSet-Cookie: admin=true";
1504        let safe = sanitize_header_value(malicious);
1505        assert_eq!(safe, "session123Set-Cookie: admin=true");
1506
1507        // Whitespace trimming
1508        assert_eq!(sanitize_header_value("  value  "), "value");
1509
1510        // Multiple newlines
1511        let malicious = "val\nue\r\nhe\na\rder";
1512        let safe = sanitize_header_value(malicious);
1513        assert_eq!(safe, "valueheader");
1514
1515        // Clean value
1516        assert_eq!(sanitize_header_value("clean-value-123"), "clean-value-123");
1517    }
1518
1519    #[test]
1520    fn test_sanitize_html_empty_and_whitespace() {
1521        assert_eq!(sanitize_html(""), "");
1522        assert_eq!(sanitize_html("   "), "   ");
1523    }
1524
1525    #[test]
1526    fn test_validate_safe_path_edge_cases() {
1527        // Single dot (current directory) - should be allowed
1528        assert!(validate_safe_path(".").is_ok());
1529
1530        // Just a filename
1531        assert!(validate_safe_path("README.md").is_ok());
1532
1533        // Deep nested path
1534        assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
1535
1536        // Multiple dots in filename (not traversal)
1537        assert!(validate_safe_path("file.test.txt").is_ok());
1538
1539        // But two consecutive dots are blocked
1540        assert!(validate_safe_path("..").is_err());
1541        assert!(validate_safe_path("dir/..").is_err());
1542    }
1543
1544    #[test]
1545    fn test_sanitize_sql_edge_cases() {
1546        // Empty string
1547        assert_eq!(sanitize_sql(""), "");
1548
1549        // Already escaped
1550        assert_eq!(sanitize_sql("''"), "''''");
1551
1552        // Multiple consecutive quotes
1553        assert_eq!(sanitize_sql("'''"), "''''''");
1554    }
1555
1556    #[test]
1557    fn test_validate_command_arg_edge_cases() {
1558        // Empty string
1559        assert!(validate_command_arg("").is_ok());
1560
1561        // Alphanumeric with dash and underscore
1562        assert!(validate_command_arg("file_name-123").is_ok());
1563
1564        // Just numbers
1565        assert!(validate_command_arg("12345").is_ok());
1566    }
1567}