mockforge_core/
validation.rs

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