1use 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#[derive(Debug)]
15pub enum Validator {
16 JsonSchema(JSONSchema),
18 OpenApi31Schema(JSONSchema, Value),
20 OpenApi(Box<OpenApiSpec>),
22 Protobuf(DescriptorPool),
24}
25
26impl Validator {
27 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 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 pub fn from_openapi(spec: &Value) -> Result<Self> {
49 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 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 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 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 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 self.validate_openapi31_schema(data, original_schema)
107 }
108 Self::OpenApi(_spec) => {
109 if data.is_object() {
111 Ok(())
113 } else {
114 Err(Error::validation("OpenAPI validation expects an object".to_string()))
115 }
116 }
117 Self::Protobuf(_) => {
118 tracing::warn!("Protobuf validation requires binary data and descriptors - use validate_protobuf() functions directly");
122 Ok(())
123 }
124 }
125 }
126
127 pub fn is_implemented(&self) -> bool {
129 match self {
130 Self::JsonSchema(_) => true,
131 Self::OpenApi31Schema(_, _) => true,
132 Self::OpenApi(_) => true, Self::Protobuf(_) => true, }
135 }
136
137 pub fn validate_openapi_ext(&self, data: &Value, openapi_schema: &Value) -> Result<()> {
139 match self {
140 Self::JsonSchema(_) => {
141 self.validate_openapi31_schema(data, openapi_schema)
143 }
144 Self::OpenApi31Schema(_, _) => {
145 self.validate_openapi31_schema(data, openapi_schema)
147 }
148 Self::OpenApi(_spec) => {
149 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 tracing::warn!("Protobuf validation requires binary data and descriptors - use validate_protobuf() functions directly");
159 Ok(())
160 }
161 }
162 }
163
164 fn validate_openapi31_schema(&self, data: &Value, schema: &Value) -> Result<()> {
166 self.validate_openapi31_constraints(data, schema, "")
167 }
168
169 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 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 _ => {} }
189 }
190
191 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if schema.get("contentEncoding").is_some() {
391 }
393
394 Ok(())
395 }
396
397 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 _ => {
436 tracing::warn!(
438 "{}: Unknown content encoding '{}', skipping validation",
439 path,
440 encoding
441 );
442 }
443 }
444
445 Ok(())
446 }
447
448 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#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
463pub struct ValidationResult {
464 pub valid: bool,
466 pub errors: Vec<String>,
468 pub warnings: Vec<String>,
470}
471
472impl ValidationResult {
473 pub fn success() -> Self {
475 Self {
476 valid: true,
477 errors: Vec::new(),
478 warnings: Vec::new(),
479 }
480 }
481
482 pub fn failure(errors: Vec<String>) -> Self {
484 Self {
485 valid: false,
486 errors,
487 warnings: Vec::new(),
488 }
489 }
490
491 pub fn with_warning(mut self, warning: String) -> Self {
493 self.warnings.push(warning);
494 self
495 }
496}
497
498pub 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
510pub fn validate_openapi(data: &Value, spec: &Value) -> ValidationResult {
512 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 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 if serde_json::from_value::<openapiv3::OpenAPI>(spec.clone()).is_ok() {
552 let _spec_wrapper = OpenApiSpec::from_json(spec.clone()).unwrap_or_else(|_| {
553 OpenApiSpec::from_json(json!({}))
555 .expect("Empty JSON object should always create valid OpenApiSpec")
556 });
557
558 if data.is_object() {
561 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
575pub 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 if let Some(path_item_ref) = spec.spec.paths.paths.get(path) {
587 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 } 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
621pub fn validate_protobuf(_data: &[u8], _descriptor_data: &[u8]) -> ValidationResult {
623 ValidationResult::failure(vec!["Protobuf validation is not yet fully implemented".to_string()])
626}
627
628pub fn validate_protobuf_message(
630 data: &[u8],
631 message_descriptor: &prost_reflect::MessageDescriptor,
632) -> Result<()> {
633 match DynamicMessage::decode(message_descriptor.clone(), data) {
635 Ok(_) => Ok(()),
636 Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
637 }
638}
639
640pub fn validate_protobuf_with_type(
642 _data: &[u8],
643 _descriptor_data: &[u8],
644 _message_type_name: &str,
645) -> ValidationResult {
646 ValidationResult::failure(vec!["Protobuf validation is not yet fully implemented".to_string()])
649}
650
651pub 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
664pub 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 let operations = spec.operations_for_path(path);
674
675 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 let openapi_operation =
692 OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
693
694 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 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 ValidationResult::success()
709}
710
711pub fn sanitize_html(input: &str) -> String {
729 input
730 .replace('&', "&")
731 .replace('<', "<")
732 .replace('>', ">")
733 .replace('"', """)
734 .replace('\'', "'")
735 .replace('/', "/")
736}
737
738pub fn validate_safe_path(path: &str) -> Result<String> {
758 if path.contains('\0') {
760 return Err(Error::validation("Path contains null bytes".to_string()));
761 }
762
763 if path.contains("..") {
765 return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
766 }
767
768 if path.contains('~') {
770 return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
771 }
772
773 if path.starts_with('/') {
775 return Err(Error::validation("Absolute paths not allowed".to_string()));
776 }
777
778 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 if path.starts_with("\\\\") || path.starts_with("//") {
785 return Err(Error::validation("UNC paths not allowed".to_string()));
786 }
787
788 let normalized = path.replace('\\', "/");
790
791 if normalized.contains("//") {
793 return Err(Error::validation("Path contains empty segments".to_string()));
794 }
795
796 Ok(normalized)
797}
798
799pub fn sanitize_sql(input: &str) -> String {
816 input.replace('\'', "''")
818}
819
820pub fn validate_command_arg(arg: &str) -> Result<String> {
843 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 if arg.contains("$(") {
860 return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
861 }
862
863 Ok(arg.to_string())
864}
865
866pub fn sanitize_json_string(input: &str) -> String {
880 input
881 .replace('\\', "\\\\") .replace('"', "\\\"")
883 .replace('\n', "\\n")
884 .replace('\r', "\\r")
885 .replace('\t', "\\t")
886}
887
888pub fn validate_url_safe(url: &str) -> Result<String> {
908 let url_lower = url.to_lowercase();
910
911 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 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 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 let metadata_endpoints = [
945 "metadata.google.internal",
946 "169.254.169.254", "fd00:ec2::254", ];
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
961pub fn sanitize_header_value(input: &str) -> String {
976 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 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 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 let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
1248 assert!(result.is_ok());
1249
1250 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 let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
1261 assert!(result.is_ok());
1262
1263 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 #[test]
1312 fn test_sanitize_html() {
1313 assert_eq!(
1315 sanitize_html("<script>alert('xss')</script>"),
1316 "<script>alert('xss')</script>"
1317 );
1318
1319 assert_eq!(
1321 sanitize_html("<img src=x onerror=\"alert(1)\">"),
1322 "<img src=x onerror="alert(1)">"
1323 );
1324
1325 assert_eq!(
1327 sanitize_html("<a href=\"javascript:void(0)\">"),
1328 "<a href="javascript:void(0)">"
1329 );
1330
1331 assert_eq!(sanitize_html("&<>"), "&<>");
1333
1334 assert_eq!(
1336 sanitize_html("Hello <b>World</b> & 'Friends'"),
1337 "Hello <b>World</b> & 'Friends'"
1338 );
1339 }
1340
1341 #[test]
1342 fn test_validate_safe_path() {
1343 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 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 assert!(validate_safe_path("~/secret").is_err());
1355 assert!(validate_safe_path("dir/~/file").is_err());
1356
1357 assert!(validate_safe_path("/etc/passwd").is_err());
1359 assert!(validate_safe_path("/var/log/app.log").is_err());
1360
1361 assert!(validate_safe_path("C:\\Windows\\System32").is_err());
1363 assert!(validate_safe_path("D:\\data\\file.txt").is_err());
1364
1365 assert!(validate_safe_path("\\\\server\\share").is_err());
1367 assert!(validate_safe_path("//server/share").is_err());
1368
1369 assert!(validate_safe_path("file\0.txt").is_err());
1371
1372 assert!(validate_safe_path("dir//file.txt").is_err());
1374
1375 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 assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
1384
1385 assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
1387
1388 assert_eq!(sanitize_sql("admin"), "admin");
1390
1391 assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
1393 }
1394
1395 #[test]
1396 fn test_validate_command_arg() {
1397 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 assert!(validate_command_arg("file | cat /etc/passwd").is_err());
1404 assert!(validate_command_arg("file || echo pwned").is_err());
1405
1406 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 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 assert!(validate_command_arg("file `whoami`").is_err());
1418 assert!(validate_command_arg("file $(whoami)").is_err());
1419
1420 assert!(validate_command_arg("file*.txt").is_err());
1422 assert!(validate_command_arg("file?.log").is_err());
1423
1424 assert!(validate_command_arg("file[0-9]").is_err());
1426 assert!(validate_command_arg("file{1,2}").is_err());
1427
1428 assert!(validate_command_arg("file\0.txt").is_err());
1430
1431 assert!(validate_command_arg("file\nrm -rf /").is_err());
1433 assert!(validate_command_arg("file\rcommand").is_err());
1434
1435 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 assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
1444
1445 assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
1447
1448 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 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 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 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 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 assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
1481
1482 assert!(validate_url_safe("http://metadata.google.internal").is_err());
1484 assert!(validate_url_safe("http://169.254.169.254").is_err());
1485
1486 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 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 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 assert_eq!(sanitize_header_value(" value "), "value");
1507
1508 let malicious = "val\nue\r\nhe\na\rder";
1510 let safe = sanitize_header_value(malicious);
1511 assert_eq!(safe, "valueheader");
1512
1513 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 assert!(validate_safe_path(".").is_ok());
1527
1528 assert!(validate_safe_path("README.md").is_ok());
1530
1531 assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
1533
1534 assert!(validate_safe_path("file.test.txt").is_ok());
1536
1537 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 assert_eq!(sanitize_sql(""), "");
1546
1547 assert_eq!(sanitize_sql("''"), "''''");
1549
1550 assert_eq!(sanitize_sql("'''"), "''''''");
1552 }
1553
1554 #[test]
1555 fn test_validate_command_arg_edge_cases() {
1556 assert!(validate_command_arg("").is_ok());
1558
1559 assert!(validate_command_arg("file_name-123").is_ok());
1561
1562 assert!(validate_command_arg("12345").is_ok());
1564 }
1565}