1use crate::{
6 openapi::{OpenApiOperation, OpenApiSecurityRequirement, OpenApiSpec},
7 Error, Result,
8};
9use base32::Alphabet;
10use base64::{engine::general_purpose, Engine as _};
11use jsonschema::{self, Draft, Validator as JSONSchema};
12use prost_reflect::{DescriptorPool, DynamicMessage};
13use serde_json::{json, Value};
14
15#[derive(Debug)]
17pub enum Validator {
18 JsonSchema(JSONSchema),
20 OpenApi31Schema(JSONSchema, Value),
22 OpenApi(Box<OpenApiSpec>),
24 Protobuf(DescriptorPool),
26}
27
28impl Validator {
29 pub fn from_json_schema(schema: &Value) -> Result<Self> {
31 let compiled = jsonschema::options()
32 .with_draft(Draft::Draft7)
33 .build(schema)
34 .map_err(|e| Error::validation(format!("Failed to compile JSON schema: {}", e)))?;
35
36 Ok(Self::JsonSchema(compiled))
37 }
38
39 pub fn from_openapi31_schema(schema: &Value) -> Result<Self> {
41 let compiled =
42 jsonschema::options().with_draft(Draft::Draft7).build(schema).map_err(|e| {
43 Error::validation(format!("Failed to compile OpenAPI 3.1 schema: {}", e))
44 })?;
45
46 Ok(Self::OpenApi31Schema(compiled, schema.clone()))
47 }
48
49 pub fn from_openapi(spec: &Value) -> Result<Self> {
51 if let Some(openapi_version) = spec.get("openapi") {
53 if let Some(version_str) = openapi_version.as_str() {
54 if !version_str.starts_with("3.") {
55 return Err(Error::validation(format!(
56 "Unsupported OpenAPI version: {}. Only 3.x is supported",
57 version_str
58 )));
59 }
60 }
61 }
62
63 let openapi_spec = OpenApiSpec::from_json(spec.clone())
65 .map_err(|e| Error::validation(format!("Failed to parse OpenAPI spec: {}", e)))?;
66
67 Ok(Self::OpenApi(Box::new(openapi_spec)))
68 }
69
70 pub fn from_protobuf(descriptor: &[u8]) -> Result<Self> {
72 let mut pool = DescriptorPool::new();
73 pool.decode_file_descriptor_set(descriptor)
74 .map_err(|e| Error::validation(format!("Invalid protobuf descriptor: {}", e)))?;
75 Ok(Self::Protobuf(pool))
76 }
77
78 pub fn validate(&self, data: &Value) -> Result<()> {
80 match self {
81 Self::JsonSchema(schema) => {
82 let mut errors = Vec::new();
83 for error in schema.iter_errors(data) {
84 errors.push(error.to_string());
85 }
86
87 if errors.is_empty() {
88 Ok(())
89 } else {
90 Err(Error::validation(format!("Validation failed: {}", errors.join(", "))))
91 }
92 }
93 Self::OpenApi31Schema(schema, original_schema) => {
94 let mut errors = Vec::new();
96 for error in schema.iter_errors(data) {
97 errors.push(error.to_string());
98 }
99
100 if !errors.is_empty() {
101 return Err(Error::validation(format!(
102 "Validation failed: {}",
103 errors.join(", ")
104 )));
105 }
106
107 self.validate_openapi31_schema(data, original_schema)
109 }
110 Self::OpenApi(_spec) => {
111 if data.is_object() {
113 Ok(())
115 } else {
116 Err(Error::validation("OpenAPI validation expects an object".to_string()))
117 }
118 }
119 Self::Protobuf(_) => {
120 tracing::warn!("Protobuf validation requires binary data and descriptors - use validate_protobuf() functions directly");
124 Ok(())
125 }
126 }
127 }
128
129 pub fn is_implemented(&self) -> bool {
131 match self {
132 Self::JsonSchema(_) => true,
133 Self::OpenApi31Schema(_, _) => true,
134 Self::OpenApi(_) => true, Self::Protobuf(_) => true, }
137 }
138
139 pub fn validate_openapi_ext(&self, data: &Value, openapi_schema: &Value) -> Result<()> {
141 match self {
142 Self::JsonSchema(_) => {
143 self.validate_openapi31_schema(data, openapi_schema)
145 }
146 Self::OpenApi31Schema(_, _) => {
147 self.validate_openapi31_schema(data, openapi_schema)
149 }
150 Self::OpenApi(_spec) => {
151 if data.is_object() {
153 Ok(())
154 } else {
155 Err(Error::validation("OpenAPI validation expects an object".to_string()))
156 }
157 }
158 Self::Protobuf(_) => {
159 tracing::warn!("Protobuf validation requires binary data and descriptors - use validate_protobuf() functions directly");
161 Ok(())
162 }
163 }
164 }
165
166 fn validate_openapi31_schema(&self, data: &Value, schema: &Value) -> Result<()> {
168 self.validate_openapi31_constraints(data, schema, "")
169 }
170
171 fn validate_openapi31_constraints(
173 &self,
174 data: &Value,
175 schema: &Value,
176 path: &str,
177 ) -> Result<()> {
178 let schema_obj = schema
179 .as_object()
180 .ok_or_else(|| Error::validation(format!("{}: Schema must be an object", path)))?;
181
182 if let Some(type_str) = schema_obj.get("type").and_then(|v| v.as_str()) {
184 match type_str {
185 "number" | "integer" => self.validate_number_constraints(data, schema_obj, path)?,
186 "array" => self.validate_array_constraints(data, schema_obj, path)?,
187 "object" => self.validate_object_constraints(data, schema_obj, path)?,
188 "string" => self.validate_string_constraints(data, schema_obj, path)?,
189 _ => {} }
191 }
192
193 if let Some(all_of) = schema_obj.get("allOf").and_then(|v| v.as_array()) {
195 for subschema in all_of {
196 self.validate_openapi31_constraints(data, subschema, path)?;
197 }
198 }
199
200 if let Some(any_of) = schema_obj.get("anyOf").and_then(|v| v.as_array()) {
201 let mut errors = Vec::new();
202 for subschema in any_of {
203 if let Err(e) = self.validate_openapi31_constraints(data, subschema, path) {
204 errors.push(e.to_string());
205 } else {
206 return Ok(());
208 }
209 }
210 if !errors.is_empty() {
211 return Err(Error::validation(format!(
212 "{}: No subschema in anyOf matched: {}",
213 path,
214 errors.join(", ")
215 )));
216 }
217 }
218
219 if let Some(one_of) = schema_obj.get("oneOf").and_then(|v| v.as_array()) {
220 let mut matches = 0;
221 for subschema in one_of {
222 if self.validate_openapi31_constraints(data, subschema, path).is_ok() {
223 matches += 1;
224 }
225 }
226 if matches != 1 {
227 return Err(Error::validation(format!(
228 "{}: Expected exactly one subschema in oneOf to match, got {}",
229 path, matches
230 )));
231 }
232 }
233
234 if let Some(content_encoding) = schema_obj.get("contentEncoding").and_then(|v| v.as_str()) {
236 self.validate_content_encoding(data.as_str(), content_encoding, path)?;
237 }
238
239 Ok(())
240 }
241
242 fn validate_number_constraints(
244 &self,
245 data: &Value,
246 schema: &serde_json::Map<String, Value>,
247 path: &str,
248 ) -> Result<()> {
249 let num = data
250 .as_f64()
251 .ok_or_else(|| Error::validation(format!("{}: Expected number, got {}", path, data)))?;
252
253 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
255 if multiple_of > 0.0 && (num / multiple_of) % 1.0 != 0.0 {
256 return Err(Error::validation(format!(
257 "{}: {} is not a multiple of {}",
258 path, num, multiple_of
259 )));
260 }
261 }
262
263 if let Some(excl_min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
265 if num <= excl_min {
266 return Err(Error::validation(format!(
267 "{}: {} must be greater than {}",
268 path, num, excl_min
269 )));
270 }
271 }
272
273 if let Some(excl_max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
275 if num >= excl_max {
276 return Err(Error::validation(format!(
277 "{}: {} must be less than {}",
278 path, num, excl_max
279 )));
280 }
281 }
282
283 Ok(())
284 }
285
286 fn validate_array_constraints(
288 &self,
289 data: &Value,
290 schema: &serde_json::Map<String, Value>,
291 path: &str,
292 ) -> Result<()> {
293 let arr = data
294 .as_array()
295 .ok_or_else(|| Error::validation(format!("{}: Expected array, got {}", path, data)))?;
296
297 if let Some(min_items) = schema.get("minItems").and_then(|v| v.as_u64()).map(|v| v as usize)
299 {
300 if arr.len() < min_items {
301 return Err(Error::validation(format!(
302 "{}: Array has {} items, minimum is {}",
303 path,
304 arr.len(),
305 min_items
306 )));
307 }
308 }
309
310 if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()).map(|v| v as usize)
312 {
313 if arr.len() > max_items {
314 return Err(Error::validation(format!(
315 "{}: Array has {} items, maximum is {}",
316 path,
317 arr.len(),
318 max_items
319 )));
320 }
321 }
322
323 if let Some(unique) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
325 if unique && !self.has_unique_items(arr) {
326 return Err(Error::validation(format!("{}: Array items must be unique", path)));
327 }
328 }
329
330 if let Some(items_schema) = schema.get("items") {
332 for (idx, item) in arr.iter().enumerate() {
333 let item_path = format!("{}[{}]", path, idx);
334 self.validate_openapi31_constraints(item, items_schema, &item_path)?;
335 }
336 }
337
338 Ok(())
339 }
340
341 fn validate_object_constraints(
343 &self,
344 data: &Value,
345 schema: &serde_json::Map<String, Value>,
346 path: &str,
347 ) -> Result<()> {
348 let obj = data
349 .as_object()
350 .ok_or_else(|| Error::validation(format!("{}: Expected object, got {}", path, data)))?;
351
352 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
354 for req_prop in required {
355 if let Some(prop_name) = req_prop.as_str() {
356 if !obj.contains_key(prop_name) {
357 return Err(Error::validation(format!(
358 "{}: Missing required property '{}'",
359 path, prop_name
360 )));
361 }
362 }
363 }
364 }
365
366 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
368 for (prop_name, prop_schema) in properties {
369 if let Some(prop_value) = obj.get(prop_name) {
370 let prop_path = format!("{}/{}", path, prop_name);
371 self.validate_openapi31_constraints(prop_value, prop_schema, &prop_path)?;
372 }
373 }
374 }
375
376 Ok(())
377 }
378
379 fn validate_string_constraints(
381 &self,
382 data: &Value,
383 schema: &serde_json::Map<String, Value>,
384 path: &str,
385 ) -> Result<()> {
386 let _str_val = data
387 .as_str()
388 .ok_or_else(|| Error::validation(format!("{}: Expected string, got {}", path, data)))?;
389
390 if schema.get("contentEncoding").is_some() {
393 }
395
396 Ok(())
397 }
398
399 fn validate_content_encoding(
401 &self,
402 data: Option<&str>,
403 encoding: &str,
404 path: &str,
405 ) -> Result<()> {
406 let str_data = data.ok_or_else(|| {
407 Error::validation(format!("{}: Content encoding requires string data", path))
408 })?;
409
410 match encoding {
411 "base64" => {
412 if general_purpose::STANDARD.decode(str_data).is_err() {
413 return Err(Error::validation(format!("{}: Invalid base64 encoding", path)));
414 }
415 }
416 "base64url" => {
417 use base64::engine::general_purpose::URL_SAFE;
418 use base64::Engine;
419 if URL_SAFE.decode(str_data).is_err() {
420 return Err(Error::validation(format!("{}: Invalid base64url encoding", path)));
421 }
422 }
423 "base32" => {
424 if base32::decode(Alphabet::Rfc4648 { padding: false }, str_data).is_none() {
425 return Err(Error::validation(format!("{}: Invalid base32 encoding", path)));
426 }
427 }
428 "hex" | "binary" => {
429 if hex::decode(str_data).is_err() {
430 return Err(Error::validation(format!(
431 "{}: Invalid {} encoding",
432 path, encoding
433 )));
434 }
435 }
436 _ => {
438 tracing::warn!(
440 "{}: Unknown content encoding '{}', skipping validation",
441 path,
442 encoding
443 );
444 }
445 }
446
447 Ok(())
448 }
449
450 fn has_unique_items(&self, arr: &[Value]) -> bool {
452 let mut seen = std::collections::HashSet::new();
453 for item in arr {
454 let item_str = serde_json::to_string(item).unwrap_or_default();
455 if !seen.insert(item_str) {
456 return false;
457 }
458 }
459 true
460 }
461}
462
463#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
465pub struct ValidationResult {
466 pub valid: bool,
468 pub errors: Vec<String>,
470 pub warnings: Vec<String>,
472}
473
474impl ValidationResult {
475 pub fn success() -> Self {
477 Self {
478 valid: true,
479 errors: Vec::new(),
480 warnings: Vec::new(),
481 }
482 }
483
484 pub fn failure(errors: Vec<String>) -> Self {
486 Self {
487 valid: false,
488 errors,
489 warnings: Vec::new(),
490 }
491 }
492
493 pub fn with_warning(mut self, warning: String) -> Self {
495 self.warnings.push(warning);
496 self
497 }
498}
499
500pub fn validate_json_schema(data: &Value, schema: &Value) -> ValidationResult {
502 match Validator::from_json_schema(schema) {
503 Ok(validator) => match validator.validate(data) {
504 Ok(_) => ValidationResult::success(),
505 Err(Error::Validation { message }) => ValidationResult::failure(vec![message]),
506 Err(e) => ValidationResult::failure(vec![format!("Unexpected error: {}", e)]),
507 },
508 Err(e) => ValidationResult::failure(vec![format!("Schema compilation error: {}", e)]),
509 }
510}
511
512pub fn validate_openapi(data: &Value, spec: &Value) -> ValidationResult {
514 let spec_obj = match spec.as_object() {
516 Some(obj) => obj,
517 None => {
518 return ValidationResult::failure(vec!["OpenAPI spec must be an object".to_string()])
519 }
520 };
521
522 let mut errors = Vec::new();
524
525 if !spec_obj.contains_key("openapi") {
526 errors.push("Missing required 'openapi' field".to_string());
527 } else if let Some(version) = spec_obj.get("openapi").and_then(|v| v.as_str()) {
528 if !version.starts_with("3.") {
529 errors.push(format!("Unsupported OpenAPI version: {}. Only 3.x is supported", version));
530 }
531 }
532
533 if !spec_obj.contains_key("info") {
534 errors.push("Missing required 'info' field".to_string());
535 } else if let Some(info) = spec_obj.get("info").and_then(|v| v.as_object()) {
536 if !info.contains_key("title") {
537 errors.push("Missing required 'info.title' field".to_string());
538 }
539 if !info.contains_key("version") {
540 errors.push("Missing required 'info.version' field".to_string());
541 }
542 }
543
544 if !spec_obj.contains_key("paths") {
545 errors.push("Missing required 'paths' field".to_string());
546 }
547
548 if !errors.is_empty() {
549 return ValidationResult::failure(errors);
550 }
551
552 if serde_json::from_value::<openapiv3::OpenAPI>(spec.clone()).is_ok() {
554 let _spec_wrapper = OpenApiSpec::from_json(spec.clone()).unwrap_or_else(|_| {
555 OpenApiSpec::from_json(json!({}))
557 .expect("Empty JSON object should always create valid OpenApiSpec")
558 });
559
560 if data.is_object() {
563 ValidationResult::success()
566 .with_warning("OpenAPI schema validation available - use validate_openapi_with_path for operation-specific validation".to_string())
567 } else {
568 ValidationResult::failure(vec![
569 "Request/response data must be a JSON object".to_string()
570 ])
571 }
572 } else {
573 ValidationResult::failure(vec!["Failed to parse OpenAPI specification".to_string()])
574 }
575}
576
577pub fn validate_openapi_operation(
579 _data: &Value,
580 spec: &OpenApiSpec,
581 path: &str,
582 method: &str,
583 _is_request: bool,
584) -> ValidationResult {
585 let mut errors = Vec::new();
586
587 if let Some(path_item_ref) = spec.spec.paths.paths.get(path) {
589 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
591 let operation = match method.to_uppercase().as_str() {
592 "GET" => path_item.get.as_ref(),
593 "POST" => path_item.post.as_ref(),
594 "PUT" => path_item.put.as_ref(),
595 "DELETE" => path_item.delete.as_ref(),
596 "PATCH" => path_item.patch.as_ref(),
597 "HEAD" => path_item.head.as_ref(),
598 "OPTIONS" => path_item.options.as_ref(),
599 _ => None,
600 };
601
602 if operation.is_some() {
603 } else {
606 errors.push(format!("Method {} not found for path {}", method, path));
607 }
608 } else {
609 errors
610 .push(format!("Path {} contains a reference, not supported for validation", path));
611 }
612 } else {
613 errors.push(format!("Path {} not found in OpenAPI spec", path));
614 }
615
616 if errors.is_empty() {
617 ValidationResult::success()
618 } else {
619 ValidationResult::failure(errors)
620 }
621}
622
623pub fn validate_protobuf(_data: &[u8], _descriptor_data: &[u8]) -> ValidationResult {
625 ValidationResult::failure(vec!["Protobuf validation is not yet fully implemented".to_string()])
628}
629
630pub fn validate_protobuf_message(
632 data: &[u8],
633 message_descriptor: &prost_reflect::MessageDescriptor,
634) -> Result<()> {
635 match DynamicMessage::decode(message_descriptor.clone(), data) {
637 Ok(_) => Ok(()),
638 Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
639 }
640}
641
642pub fn validate_protobuf_with_type(
644 _data: &[u8],
645 _descriptor_data: &[u8],
646 _message_type_name: &str,
647) -> ValidationResult {
648 ValidationResult::failure(vec!["Protobuf validation is not yet fully implemented".to_string()])
651}
652
653pub fn validate_openapi_security(
655 spec: &OpenApiSpec,
656 security_requirements: &[OpenApiSecurityRequirement],
657 auth_header: Option<&str>,
658 api_key: Option<&str>,
659) -> ValidationResult {
660 match spec.validate_security_requirements(security_requirements, auth_header, api_key) {
661 Ok(_) => ValidationResult::success(),
662 Err(e) => ValidationResult::failure(vec![format!("Security validation failed: {}", e)]),
663 }
664}
665
666pub fn validate_openapi_operation_security(
668 spec: &OpenApiSpec,
669 path: &str,
670 method: &str,
671 auth_header: Option<&str>,
672 api_key: Option<&str>,
673) -> ValidationResult {
674 let operations = spec.operations_for_path(path);
676
677 let operation = operations
679 .iter()
680 .find(|(op_method, _)| op_method.to_uppercase() == method.to_uppercase());
681
682 let operation = match operation {
683 Some((_, op)) => op,
684 None => {
685 return ValidationResult::failure(vec![format!(
686 "Operation not found: {} {}",
687 method, path
688 )])
689 }
690 };
691
692 let openapi_operation =
694 OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
695
696 if let Some(ref security_reqs) = openapi_operation.security {
698 if !security_reqs.is_empty() {
699 return validate_openapi_security(spec, security_reqs, auth_header, api_key);
700 }
701 }
702
703 let global_security = spec.get_global_security_requirements();
705 if !global_security.is_empty() {
706 return validate_openapi_security(spec, &global_security, auth_header, api_key);
707 }
708
709 ValidationResult::success()
711}
712
713pub fn sanitize_html(input: &str) -> String {
731 input
732 .replace('&', "&")
733 .replace('<', "<")
734 .replace('>', ">")
735 .replace('"', """)
736 .replace('\'', "'")
737 .replace('/', "/")
738}
739
740pub fn validate_safe_path(path: &str) -> Result<String> {
760 if path.contains('\0') {
762 return Err(Error::validation("Path contains null bytes".to_string()));
763 }
764
765 if path.contains("..") {
767 return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
768 }
769
770 if path.contains('~') {
772 return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
773 }
774
775 if path.starts_with('/') {
777 return Err(Error::validation("Absolute paths not allowed".to_string()));
778 }
779
780 if path.len() >= 2 && path.chars().nth(1) == Some(':') {
782 return Err(Error::validation("Absolute paths with drive letters not allowed".to_string()));
783 }
784
785 if path.starts_with("\\\\") || path.starts_with("//") {
787 return Err(Error::validation("UNC paths not allowed".to_string()));
788 }
789
790 let normalized = path.replace('\\', "/");
792
793 if normalized.contains("//") {
795 return Err(Error::validation("Path contains empty segments".to_string()));
796 }
797
798 Ok(normalized)
799}
800
801pub fn sanitize_sql(input: &str) -> String {
818 input.replace('\'', "''")
820}
821
822pub fn validate_command_arg(arg: &str) -> Result<String> {
845 let dangerous_chars = [
847 '|', ';', '&', '<', '>', '`', '$', '(', ')', '*', '?', '[', ']', '{', '}', '~', '!', '\n',
848 '\r', '\0',
849 ];
850
851 for ch in dangerous_chars.iter() {
852 if arg.contains(*ch) {
853 return Err(Error::validation(format!(
854 "Command argument contains dangerous character: '{}'",
855 ch
856 )));
857 }
858 }
859
860 if arg.contains("$(") {
862 return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
863 }
864
865 Ok(arg.to_string())
866}
867
868pub fn sanitize_json_string(input: &str) -> String {
882 input
883 .replace('\\', "\\\\") .replace('"', "\\\"")
885 .replace('\n', "\\n")
886 .replace('\r', "\\r")
887 .replace('\t', "\\t")
888}
889
890pub fn validate_url_safe(url: &str) -> Result<String> {
910 let url_lower = url.to_lowercase();
912
913 let localhost_patterns = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"];
915 for pattern in localhost_patterns.iter() {
916 if url_lower.contains(pattern) {
917 return Err(Error::validation(
918 "URLs pointing to localhost are not allowed".to_string(),
919 ));
920 }
921 }
922
923 let private_ranges = [
925 "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.",
926 "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.",
927 "172.31.", "192.168.",
928 ];
929 for range in private_ranges.iter() {
930 if url_lower.contains(range) {
931 return Err(Error::validation(format!(
932 "URLs pointing to private IP range '{}' are not allowed",
933 range
934 )));
935 }
936 }
937
938 if url_lower.contains("169.254.") {
940 return Err(Error::validation(
941 "URLs pointing to link-local addresses (169.254.x) are not allowed".to_string(),
942 ));
943 }
944
945 let metadata_endpoints = [
947 "metadata.google.internal",
948 "169.254.169.254", "fd00:ec2::254", ];
951 for endpoint in metadata_endpoints.iter() {
952 if url_lower.contains(endpoint) {
953 return Err(Error::validation(format!(
954 "URLs pointing to cloud metadata endpoint '{}' are not allowed",
955 endpoint
956 )));
957 }
958 }
959
960 Ok(url.to_string())
961}
962
963pub fn sanitize_header_value(input: &str) -> String {
978 input.replace(['\r', '\n'], "").trim().to_string()
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[test]
987 fn test_validation_result_success() {
988 let result = ValidationResult::success();
989 assert!(result.valid);
990 assert!(result.errors.is_empty());
991 assert!(result.warnings.is_empty());
992 }
993
994 #[test]
995 fn test_validation_result_failure() {
996 let errors = vec!["error1".to_string(), "error2".to_string()];
997 let result = ValidationResult::failure(errors.clone());
998 assert!(!result.valid);
999 assert_eq!(result.errors, errors);
1000 assert!(result.warnings.is_empty());
1001 }
1002
1003 #[test]
1004 fn test_validation_result_with_warning() {
1005 let result = ValidationResult::success()
1006 .with_warning("warning1".to_string())
1007 .with_warning("warning2".to_string());
1008 assert!(result.valid);
1009 assert_eq!(result.warnings.len(), 2);
1010 }
1011
1012 #[test]
1013 fn test_validator_from_json_schema() {
1014 let schema = json!({
1015 "type": "object",
1016 "properties": {
1017 "name": {"type": "string"}
1018 }
1019 });
1020
1021 let validator = Validator::from_json_schema(&schema);
1022 assert!(validator.is_ok());
1023 assert!(validator.unwrap().is_implemented());
1024 }
1025
1026 #[test]
1027 fn test_validator_from_json_schema_invalid() {
1028 let schema = json!({
1029 "type": "invalid_type"
1030 });
1031
1032 let validator = Validator::from_json_schema(&schema);
1034 assert!(validator.is_err());
1035 }
1036
1037 #[test]
1038 fn test_validator_validate_json_schema_success() {
1039 let schema = json!({
1040 "type": "object",
1041 "properties": {
1042 "name": {"type": "string"}
1043 }
1044 });
1045
1046 let validator = Validator::from_json_schema(&schema).unwrap();
1047 let data = json!({"name": "test"});
1048
1049 assert!(validator.validate(&data).is_ok());
1050 }
1051
1052 #[test]
1053 fn test_validator_validate_json_schema_failure() {
1054 let schema = json!({
1055 "type": "object",
1056 "properties": {
1057 "name": {"type": "string"}
1058 }
1059 });
1060
1061 let validator = Validator::from_json_schema(&schema).unwrap();
1062 let data = json!({"name": 123});
1063
1064 assert!(validator.validate(&data).is_err());
1065 }
1066
1067 #[test]
1068 fn test_validator_from_openapi() {
1069 let spec = json!({
1070 "openapi": "3.0.0",
1071 "info": {"title": "Test", "version": "1.0.0"},
1072 "paths": {}
1073 });
1074
1075 let validator = Validator::from_openapi(&spec);
1076 assert!(validator.is_ok());
1077 }
1078
1079 #[test]
1080 fn test_validator_from_openapi_unsupported_version() {
1081 let spec = json!({
1082 "openapi": "2.0.0",
1083 "info": {"title": "Test", "version": "1.0.0"},
1084 "paths": {}
1085 });
1086
1087 let validator = Validator::from_openapi(&spec);
1088 assert!(validator.is_err());
1089 }
1090
1091 #[test]
1092 fn test_validator_validate_openapi() {
1093 let spec = json!({
1094 "openapi": "3.0.0",
1095 "info": {"title": "Test", "version": "1.0.0"},
1096 "paths": {}
1097 });
1098
1099 let validator = Validator::from_openapi(&spec).unwrap();
1100 let data = json!({"key": "value"});
1101
1102 assert!(validator.validate(&data).is_ok());
1103 }
1104
1105 #[test]
1106 fn test_validator_validate_openapi_non_object() {
1107 let spec = json!({
1108 "openapi": "3.0.0",
1109 "info": {"title": "Test", "version": "1.0.0"},
1110 "paths": {}
1111 });
1112
1113 let validator = Validator::from_openapi(&spec).unwrap();
1114 let data = json!("string");
1115
1116 assert!(validator.validate(&data).is_err());
1117 }
1118
1119 #[test]
1120 fn test_validate_json_schema_function() {
1121 let schema = json!({
1122 "type": "object",
1123 "properties": {
1124 "age": {"type": "number"}
1125 }
1126 });
1127
1128 let data = json!({"age": 25});
1129 let result = validate_json_schema(&data, &schema);
1130 assert!(result.valid);
1131
1132 let data = json!({"age": "25"});
1133 let result = validate_json_schema(&data, &schema);
1134 assert!(!result.valid);
1135 }
1136
1137 #[test]
1138 fn test_validate_openapi_function() {
1139 let spec = json!({
1140 "openapi": "3.0.0",
1141 "info": {"title": "Test", "version": "1.0.0"},
1142 "paths": {}
1143 });
1144
1145 let data = json!({"test": "value"});
1146 let result = validate_openapi(&data, &spec);
1147 assert!(result.valid);
1148 }
1149
1150 #[test]
1151 fn test_validate_openapi_missing_fields() {
1152 let spec = json!({
1153 "openapi": "3.0.0"
1154 });
1155
1156 let data = json!({});
1157 let result = validate_openapi(&data, &spec);
1158 assert!(!result.valid);
1159 assert!(!result.errors.is_empty());
1160 }
1161
1162 #[test]
1163 fn test_validate_number_constraints_multiple_of() {
1164 let schema = json!({
1165 "type": "number",
1166 "multipleOf": 5.0
1167 });
1168
1169 let validator = Validator::from_json_schema(&schema).unwrap();
1170
1171 let data = json!(10);
1172 assert!(validator.validate(&data).is_ok());
1173
1174 let data = json!(11);
1175 let _ = validator.validate(&data);
1178 }
1179
1180 #[test]
1181 fn test_validate_array_constraints_min_items() {
1182 let schema = json!({
1183 "type": "array",
1184 "minItems": 2
1185 });
1186
1187 let validator = Validator::from_json_schema(&schema).unwrap();
1188
1189 let data = json!([1, 2]);
1190 assert!(validator.validate(&data).is_ok());
1191
1192 let data = json!([1]);
1193 assert!(validator.validate(&data).is_err());
1194 }
1195
1196 #[test]
1197 fn test_validate_array_constraints_max_items() {
1198 let schema = json!({
1199 "type": "array",
1200 "maxItems": 2
1201 });
1202
1203 let validator = Validator::from_json_schema(&schema).unwrap();
1204
1205 let data = json!([1]);
1206 assert!(validator.validate(&data).is_ok());
1207
1208 let data = json!([1, 2, 3]);
1209 assert!(validator.validate(&data).is_err());
1210 }
1211
1212 #[test]
1213 fn test_validate_array_unique_items() {
1214 let schema = json!({
1215 "type": "array",
1216 "uniqueItems": true
1217 });
1218
1219 let validator = Validator::from_json_schema(&schema).unwrap();
1220
1221 let data = json!([1, 2, 3]);
1222 assert!(validator.validate(&data).is_ok());
1223
1224 let data = json!([1, 2, 2]);
1225 assert!(validator.validate(&data).is_err());
1226 }
1227
1228 #[test]
1229 fn test_validate_object_required_properties() {
1230 let schema = json!({
1231 "type": "object",
1232 "required": ["name", "age"]
1233 });
1234
1235 let validator = Validator::from_json_schema(&schema).unwrap();
1236
1237 let data = json!({"name": "test", "age": 25});
1238 assert!(validator.validate(&data).is_ok());
1239
1240 let data = json!({"name": "test"});
1241 assert!(validator.validate(&data).is_err());
1242 }
1243
1244 #[test]
1245 fn test_validate_content_encoding_base64() {
1246 let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1247
1248 let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
1250 assert!(result.is_ok());
1251
1252 let result = validator.validate_content_encoding(Some("not-base64!@#"), "base64", "test");
1254 assert!(result.is_err());
1255 }
1256
1257 #[test]
1258 fn test_validate_content_encoding_hex() {
1259 let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1260
1261 let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
1263 assert!(result.is_ok());
1264
1265 let result = validator.validate_content_encoding(Some("xyz"), "hex", "test");
1267 assert!(result.is_err());
1268 }
1269
1270 #[test]
1271 fn test_has_unique_items() {
1272 let validator = Validator::from_json_schema(&json!({})).unwrap();
1273
1274 let arr = vec![json!(1), json!(2), json!(3)];
1275 assert!(validator.has_unique_items(&arr));
1276
1277 let arr = vec![json!(1), json!(2), json!(1)];
1278 assert!(!validator.has_unique_items(&arr));
1279 }
1280
1281 #[test]
1282 fn test_validate_protobuf() {
1283 let result = validate_protobuf(&[], &[]);
1284 assert!(!result.valid);
1285 assert!(result.errors[0].contains("not yet fully implemented"));
1286 }
1287
1288 #[test]
1289 fn test_validate_protobuf_with_type() {
1290 let result = validate_protobuf_with_type(&[], &[], "TestMessage");
1291 assert!(!result.valid);
1292 assert!(result.errors[0].contains("not yet fully implemented"));
1293 }
1294
1295 #[test]
1296 fn test_is_implemented() {
1297 let json_validator = Validator::from_json_schema(&json!({"type": "object"})).unwrap();
1298 assert!(json_validator.is_implemented());
1299
1300 let openapi_validator = Validator::from_openapi(&json!({
1301 "openapi": "3.0.0",
1302 "info": {"title": "Test", "version": "1.0.0"},
1303 "paths": {}
1304 }))
1305 .unwrap();
1306 assert!(openapi_validator.is_implemented());
1307 }
1308
1309 #[test]
1314 fn test_sanitize_html() {
1315 assert_eq!(
1317 sanitize_html("<script>alert('xss')</script>"),
1318 "<script>alert('xss')</script>"
1319 );
1320
1321 assert_eq!(
1323 sanitize_html("<img src=x onerror=\"alert(1)\">"),
1324 "<img src=x onerror="alert(1)">"
1325 );
1326
1327 assert_eq!(
1329 sanitize_html("<a href=\"javascript:void(0)\">"),
1330 "<a href="javascript:void(0)">"
1331 );
1332
1333 assert_eq!(sanitize_html("&<>"), "&<>");
1335
1336 assert_eq!(
1338 sanitize_html("Hello <b>World</b> & 'Friends'"),
1339 "Hello <b>World</b> & 'Friends'"
1340 );
1341 }
1342
1343 #[test]
1344 fn test_validate_safe_path() {
1345 assert!(validate_safe_path("data/file.txt").is_ok());
1347 assert!(validate_safe_path("subdir/file.json").is_ok());
1348 assert!(validate_safe_path("file.txt").is_ok());
1349
1350 assert!(validate_safe_path("../etc/passwd").is_err());
1352 assert!(validate_safe_path("dir/../../../etc/passwd").is_err());
1353 assert!(validate_safe_path("./../../secret").is_err());
1354
1355 assert!(validate_safe_path("~/secret").is_err());
1357 assert!(validate_safe_path("dir/~/file").is_err());
1358
1359 assert!(validate_safe_path("/etc/passwd").is_err());
1361 assert!(validate_safe_path("/var/log/app.log").is_err());
1362
1363 assert!(validate_safe_path("C:\\Windows\\System32").is_err());
1365 assert!(validate_safe_path("D:\\data\\file.txt").is_err());
1366
1367 assert!(validate_safe_path("\\\\server\\share").is_err());
1369 assert!(validate_safe_path("//server/share").is_err());
1370
1371 assert!(validate_safe_path("file\0.txt").is_err());
1373
1374 assert!(validate_safe_path("dir//file.txt").is_err());
1376
1377 let result = validate_safe_path("dir\\subdir\\file.txt").unwrap();
1379 assert_eq!(result, "dir/subdir/file.txt");
1380 }
1381
1382 #[test]
1383 fn test_sanitize_sql() {
1384 assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
1386
1387 assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
1389
1390 assert_eq!(sanitize_sql("admin"), "admin");
1392
1393 assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
1395 }
1396
1397 #[test]
1398 fn test_validate_command_arg() {
1399 assert!(validate_command_arg("safe_filename.txt").is_ok());
1401 assert!(validate_command_arg("file-123.log").is_ok());
1402 assert!(validate_command_arg("data.json").is_ok());
1403
1404 assert!(validate_command_arg("file | cat /etc/passwd").is_err());
1406 assert!(validate_command_arg("file || echo pwned").is_err());
1407
1408 assert!(validate_command_arg("file; rm -rf /").is_err());
1410 assert!(validate_command_arg("file & background").is_err());
1411 assert!(validate_command_arg("file && next").is_err());
1412
1413 assert!(validate_command_arg("file > /dev/null").is_err());
1415 assert!(validate_command_arg("file < input.txt").is_err());
1416 assert!(validate_command_arg("file >> log.txt").is_err());
1417
1418 assert!(validate_command_arg("file `whoami`").is_err());
1420 assert!(validate_command_arg("file $(whoami)").is_err());
1421
1422 assert!(validate_command_arg("file*.txt").is_err());
1424 assert!(validate_command_arg("file?.log").is_err());
1425
1426 assert!(validate_command_arg("file[0-9]").is_err());
1428 assert!(validate_command_arg("file{1,2}").is_err());
1429
1430 assert!(validate_command_arg("file\0.txt").is_err());
1432
1433 assert!(validate_command_arg("file\nrm -rf /").is_err());
1435 assert!(validate_command_arg("file\rcommand").is_err());
1436
1437 assert!(validate_command_arg("file~").is_err());
1439 assert!(validate_command_arg("file!").is_err());
1440 }
1441
1442 #[test]
1443 fn test_sanitize_json_string() {
1444 assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
1446
1447 assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
1449
1450 assert_eq!(sanitize_json_string("line1\nline2"), r#"line1\nline2"#);
1452 assert_eq!(sanitize_json_string("tab\there"), r#"tab\there"#);
1453 assert_eq!(sanitize_json_string("carriage\rreturn"), r#"carriage\rreturn"#);
1454
1455 assert_eq!(
1457 sanitize_json_string("Test\"value\"\nNext\\line"),
1458 r#"Test\"value\"\nNext\\line"#
1459 );
1460 }
1461
1462 #[test]
1463 fn test_validate_url_safe() {
1464 assert!(validate_url_safe("https://example.com").is_ok());
1466 assert!(validate_url_safe("http://api.example.com/data").is_ok());
1467 assert!(validate_url_safe("https://subdomain.example.org:8080/path").is_ok());
1468
1469 assert!(validate_url_safe("http://localhost:8080").is_err());
1471 assert!(validate_url_safe("http://127.0.0.1").is_err());
1472 assert!(validate_url_safe("http://[::1]:8080").is_err());
1473 assert!(validate_url_safe("http://0.0.0.0").is_err());
1474
1475 assert!(validate_url_safe("http://10.0.0.1").is_err());
1477 assert!(validate_url_safe("http://192.168.1.1").is_err());
1478 assert!(validate_url_safe("http://172.16.0.1").is_err());
1479 assert!(validate_url_safe("http://172.31.255.255").is_err());
1480
1481 assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
1483
1484 assert!(validate_url_safe("http://metadata.google.internal").is_err());
1486 assert!(validate_url_safe("http://169.254.169.254").is_err());
1487
1488 assert!(validate_url_safe("HTTP://LOCALHOST:8080").is_err());
1490 assert!(validate_url_safe("http://LocalHost").is_err());
1491 }
1492
1493 #[test]
1494 fn test_sanitize_header_value() {
1495 let malicious = "value\r\nX-Evil-Header: injected";
1497 let safe = sanitize_header_value(malicious);
1498 assert!(!safe.contains('\r'));
1499 assert!(!safe.contains('\n'));
1500 assert_eq!(safe, "valueX-Evil-Header: injected");
1501
1502 let malicious = "session123\r\nSet-Cookie: admin=true";
1504 let safe = sanitize_header_value(malicious);
1505 assert_eq!(safe, "session123Set-Cookie: admin=true");
1506
1507 assert_eq!(sanitize_header_value(" value "), "value");
1509
1510 let malicious = "val\nue\r\nhe\na\rder";
1512 let safe = sanitize_header_value(malicious);
1513 assert_eq!(safe, "valueheader");
1514
1515 assert_eq!(sanitize_header_value("clean-value-123"), "clean-value-123");
1517 }
1518
1519 #[test]
1520 fn test_sanitize_html_empty_and_whitespace() {
1521 assert_eq!(sanitize_html(""), "");
1522 assert_eq!(sanitize_html(" "), " ");
1523 }
1524
1525 #[test]
1526 fn test_validate_safe_path_edge_cases() {
1527 assert!(validate_safe_path(".").is_ok());
1529
1530 assert!(validate_safe_path("README.md").is_ok());
1532
1533 assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
1535
1536 assert!(validate_safe_path("file.test.txt").is_ok());
1538
1539 assert!(validate_safe_path("..").is_err());
1541 assert!(validate_safe_path("dir/..").is_err());
1542 }
1543
1544 #[test]
1545 fn test_sanitize_sql_edge_cases() {
1546 assert_eq!(sanitize_sql(""), "");
1548
1549 assert_eq!(sanitize_sql("''"), "''''");
1551
1552 assert_eq!(sanitize_sql("'''"), "''''''");
1554 }
1555
1556 #[test]
1557 fn test_validate_command_arg_edge_cases() {
1558 assert!(validate_command_arg("").is_ok());
1560
1561 assert!(validate_command_arg("file_name-123").is_ok());
1563
1564 assert!(validate_command_arg("12345").is_ok());
1566 }
1567}