1use crate::models::{Project, Specification, Task, TaskList, TechStack, Vision};
4use chrono::Utc;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct ValidationError {
10 pub field: String,
11 pub value: String,
12 pub error_type: ValidationErrorType,
13 pub message: String,
14 pub suggestion: Option<String>,
15}
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum ValidationErrorType {
20 Required,
21 TooLong,
22 TooShort,
23 InvalidFormat,
24 InvalidCharacters,
25 AlreadyExists,
26 SecurityRisk,
27 InvalidRange,
28 InvalidTimestamp,
29}
30
31impl ValidationError {
32 pub fn new(field: &str, value: &str, error_type: ValidationErrorType, message: &str) -> Self {
33 Self {
34 field: field.to_string(),
35 value: value.to_string(),
36 error_type,
37 message: message.to_string(),
38 suggestion: None,
39 }
40 }
41
42 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
43 self.suggestion = Some(suggestion.to_string());
44 self
45 }
46
47 pub fn format_error(&self) -> String {
48 let mut error_msg = format!("Field '{}': {}", self.field, self.message);
49
50 if let Some(suggestion) = &self.suggestion {
51 error_msg.push_str(&format!("\nSuggestion: {}", suggestion));
52 }
53
54 error_msg
55 }
56}
57
58pub type ValidationResult = Result<(), Vec<ValidationError>>;
60
61pub struct ValidationContext {
63 pub existing_projects: Vec<String>,
64 pub existing_specs: HashMap<String, Vec<String>>, pub strict_mode: bool,
66 pub max_content_length: usize,
67}
68
69impl Default for ValidationContext {
70 fn default() -> Self {
71 Self {
72 existing_projects: Vec::new(),
73 existing_specs: HashMap::new(),
74 strict_mode: false,
75 max_content_length: 50_000,
76 }
77 }
78}
79
80pub fn validate_project_enhanced(project: &Project, context: &ValidationContext) -> ValidationResult {
82 let mut errors = Vec::new();
83
84 if project.name.trim().is_empty() {
86 errors.push(
87 ValidationError::new(
88 "name",
89 &project.name,
90 ValidationErrorType::Required,
91 "Project name is required"
92 ).with_suggestion("Provide a descriptive name in lowercase with hyphens or underscores, e.g., 'my-awesome-project'")
93 );
94 } else {
95 if project.name.len() > 100 {
97 errors.push(
98 ValidationError::new(
99 "name",
100 &project.name,
101 ValidationErrorType::TooLong,
102 "Project name cannot exceed 100 characters"
103 ).with_suggestion("Use a shorter, more concise name")
104 );
105 }
106
107 if !project.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
109 errors.push(
110 ValidationError::new(
111 "name",
112 &project.name,
113 ValidationErrorType::InvalidCharacters,
114 "Project name can only contain letters, numbers, hyphens, and underscores"
115 ).with_suggestion("Replace special characters with hyphens or underscores")
116 );
117 }
118
119 if project.name.chars().any(|c| c.is_uppercase()) {
121 errors.push(
122 ValidationError::new(
123 "name",
124 &project.name,
125 ValidationErrorType::InvalidFormat,
126 "Project name should be lowercase"
127 ).with_suggestion("Convert the name to lowercase")
128 );
129 }
130
131 if context.existing_projects.contains(&project.name) {
133 errors.push(
134 ValidationError::new(
135 "name",
136 &project.name,
137 ValidationErrorType::AlreadyExists,
138 "Project name already exists"
139 ).with_suggestion("Choose a different project name or load the existing project")
140 );
141 }
142
143 if let Err(security_error) = validate_input_security(&project.name) {
145 errors.push(
146 ValidationError::new(
147 "name",
148 &project.name,
149 ValidationErrorType::SecurityRisk,
150 &security_error
151 ).with_suggestion("Use only safe characters in project names")
152 );
153 }
154 }
155
156 if project.description.trim().is_empty() {
158 errors.push(
159 ValidationError::new(
160 "description",
161 &project.description,
162 ValidationErrorType::Required,
163 "Project description is required"
164 ).with_suggestion("Provide a brief description of what this project does")
165 );
166 } else if project.description.len() > 2000 {
167 errors.push(
168 ValidationError::new(
169 "description",
170 &project.description,
171 ValidationErrorType::TooLong,
172 "Project description cannot exceed 2000 characters"
173 ).with_suggestion("Shorten the description or move detailed information to the vision overview")
174 );
175 }
176
177 if let Err(security_error) = validate_input_security(&project.description) {
179 errors.push(
180 ValidationError::new(
181 "description",
182 &project.description,
183 ValidationErrorType::SecurityRisk,
184 &security_error
185 ).with_suggestion("Remove any potentially dangerous content from the description")
186 );
187 }
188
189 if project.created_at > project.updated_at {
191 errors.push(
192 ValidationError::new(
193 "timestamps",
194 &format!("created: {}, updated: {}", project.created_at, project.updated_at),
195 ValidationErrorType::InvalidTimestamp,
196 "Created timestamp cannot be after updated timestamp"
197 ).with_suggestion("Ensure the updated timestamp is equal to or after the created timestamp")
198 );
199 }
200
201 if let Err(tech_stack_errors) = validate_tech_stack_enhanced(&project.tech_stack, context) {
203 errors.extend(tech_stack_errors);
204 }
205
206 if let Err(vision_errors) = validate_vision_enhanced(&project.vision, context) {
207 errors.extend(vision_errors);
208 }
209
210 if errors.is_empty() {
211 Ok(())
212 } else {
213 Err(errors)
214 }
215}
216
217pub fn validate_tech_stack_enhanced(tech_stack: &TechStack, context: &ValidationContext) -> ValidationResult {
219 let mut errors = Vec::new();
220
221 if tech_stack.languages.is_empty() && context.strict_mode {
223 errors.push(
224 ValidationError::new(
225 "languages",
226 "",
227 ValidationErrorType::Required,
228 "At least one programming language should be specified"
229 ).with_suggestion("Add the main programming language(s) for this project")
230 );
231 }
232
233 for (i, language) in tech_stack.languages.iter().enumerate() {
234 let field_name = format!("languages[{}]", i);
235 if language.trim().is_empty() {
236 errors.push(
237 ValidationError::new(
238 &field_name,
239 language,
240 ValidationErrorType::Required,
241 "Language name cannot be empty"
242 ).with_suggestion("Remove empty entries or provide a valid language name")
243 );
244 } else if language.len() > 50 {
245 errors.push(
246 ValidationError::new(
247 &field_name,
248 language,
249 ValidationErrorType::TooLong,
250 "Language name cannot exceed 50 characters"
251 ).with_suggestion("Use standard language names like 'JavaScript', 'Python', 'Rust'")
252 );
253 } else if let Err(security_error) = validate_input_security(language) {
254 errors.push(
255 ValidationError::new(
256 &field_name,
257 language,
258 ValidationErrorType::SecurityRisk,
259 &security_error
260 ).with_suggestion("Use only standard programming language names")
261 );
262 }
263 }
264
265 validate_string_list(&tech_stack.frameworks, "frameworks", 100, &mut errors, context);
267 validate_string_list(&tech_stack.databases, "databases", 100, &mut errors, context);
268 validate_string_list(&tech_stack.tools, "tools", 100, &mut errors, context);
269 validate_string_list(&tech_stack.deployment, "deployment", 100, &mut errors, context);
270
271 if errors.is_empty() {
272 Ok(())
273 } else {
274 Err(errors)
275 }
276}
277
278pub fn validate_vision_enhanced(vision: &Vision, context: &ValidationContext) -> ValidationResult {
280 let mut errors = Vec::new();
281
282 if vision.overview.trim().is_empty() {
284 errors.push(
285 ValidationError::new(
286 "overview",
287 &vision.overview,
288 ValidationErrorType::Required,
289 "Vision overview is required"
290 ).with_suggestion("Provide a clear overview of the project's purpose and scope")
291 );
292 } else if vision.overview.len() > 5000 {
293 errors.push(
294 ValidationError::new(
295 "overview",
296 &vision.overview,
297 ValidationErrorType::TooLong,
298 "Vision overview cannot exceed 5000 characters"
299 ).with_suggestion("Keep the overview concise and move detailed information to specifications")
300 );
301 }
302
303 if let Err(security_error) = validate_input_security(&vision.overview) {
305 errors.push(
306 ValidationError::new(
307 "overview",
308 &vision.overview,
309 ValidationErrorType::SecurityRisk,
310 &security_error
311 ).with_suggestion("Remove any potentially dangerous content from the overview")
312 );
313 }
314
315 validate_string_list(&vision.goals, "goals", 500, &mut errors, context);
317 validate_string_list(&vision.target_users, "target_users", 200, &mut errors, context);
318 validate_string_list(&vision.success_criteria, "success_criteria", 500, &mut errors, context);
319
320 if context.strict_mode {
322 if vision.goals.is_empty() {
323 errors.push(
324 ValidationError::new(
325 "goals",
326 "",
327 ValidationErrorType::Required,
328 "At least one goal must be specified"
329 ).with_suggestion("Define clear, measurable goals for the project")
330 );
331 }
332
333 if vision.target_users.is_empty() {
334 errors.push(
335 ValidationError::new(
336 "target_users",
337 "",
338 ValidationErrorType::Required,
339 "At least one target user must be specified"
340 ).with_suggestion("Identify who will use or benefit from this project")
341 );
342 }
343
344 if vision.success_criteria.is_empty() {
345 errors.push(
346 ValidationError::new(
347 "success_criteria",
348 "",
349 ValidationErrorType::Required,
350 "At least one success criterion must be specified"
351 ).with_suggestion("Define how you will measure the success of this project")
352 );
353 }
354 }
355
356 if errors.is_empty() {
357 Ok(())
358 } else {
359 Err(errors)
360 }
361}
362
363fn validate_string_list(
365 list: &[String],
366 field_name: &str,
367 max_length: usize,
368 errors: &mut Vec<ValidationError>,
369 _context: &ValidationContext,
370) {
371 for (i, item) in list.iter().enumerate() {
372 let item_field = format!("{}[{}]", field_name, i);
373
374 if item.trim().is_empty() {
375 errors.push(
376 ValidationError::new(
377 &item_field,
378 item,
379 ValidationErrorType::Required,
380 &format!("{} item cannot be empty", field_name)
381 ).with_suggestion("Remove empty entries or provide valid content")
382 );
383 } else if item.len() > max_length {
384 errors.push(
385 ValidationError::new(
386 &item_field,
387 item,
388 ValidationErrorType::TooLong,
389 &format!("{} item cannot exceed {} characters", field_name, max_length)
390 ).with_suggestion("Use shorter, more concise descriptions")
391 );
392 } else if let Err(security_error) = validate_input_security(item) {
393 errors.push(
394 ValidationError::new(
395 &item_field,
396 item,
397 ValidationErrorType::SecurityRisk,
398 &security_error
399 ).with_suggestion("Remove any potentially dangerous content")
400 );
401 }
402 }
403}
404
405pub fn validate_input_security(input: &str) -> Result<(), String> {
407 let dangerous_patterns = [
409 "<script", "</script>", "javascript:", "data:", "vbscript:",
410 "onload=", "onerror=", "onclick=", "onmouseover=",
411 "eval(", "setTimeout(", "setInterval(",
412 "document.cookie", "document.location", "window.location",
413 "innerHTML", "outerHTML", "document.write",
414 "exec(", "system(", "shell_exec(", "passthru(",
415 "file_get_contents(", "file_put_contents(", "fopen(",
416 "include(", "include_once(", "require(", "require_once(",
417 "<?php", "<?=", "<%", "%>", "<%=",
418 "DROP TABLE", "DELETE FROM", "INSERT INTO", "UPDATE SET",
419 "UNION SELECT", "OR 1=1", "' OR '1'='1", "\" OR \"1\"=\"1",
420 "../", "..\\", "/etc/", "/var/", "/usr/", "/bin/",
421 "C:\\Windows", "C:\\Program Files", "C:\\Users",
422 ];
423
424 let input_lower = input.to_lowercase();
425 for pattern in &dangerous_patterns {
426 if input_lower.contains(&pattern.to_lowercase()) {
427 return Err(format!("Input contains potentially dangerous pattern: {}", pattern));
428 }
429 }
430
431 if input.chars().any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t') {
433 return Err("Input contains control characters".to_string());
434 }
435
436 if input.lines().any(|line| line.len() > 10000) {
438 return Err("Input contains excessively long lines".to_string());
439 }
440
441 Ok(())
442}
443
444pub fn validate_project(project: &Project) -> Result<(), Vec<String>> {
446 let context = ValidationContext::default();
447 match validate_project_enhanced(project, &context) {
448 Ok(()) => Ok(()),
449 Err(errors) => Err(errors.into_iter().map(|e| e.format_error()).collect()),
450 }
451}
452
453pub fn validate_tech_stack(tech_stack: &TechStack) -> Result<(), Vec<String>> {
455 let mut errors = Vec::new();
456
457 errors.extend(
459 tech_stack
460 .languages
461 .iter()
462 .enumerate()
463 .filter_map(|(i, language)| {
464 if language.trim().is_empty() {
465 Some(format!("Language {} cannot be empty", i + 1))
466 } else if language.len() > 50 {
467 Some(format!(
468 "Language '{}' cannot exceed 50 characters",
469 language
470 ))
471 } else {
472 None
473 }
474 }),
475 );
476
477 errors.extend(
479 tech_stack
480 .frameworks
481 .iter()
482 .enumerate()
483 .filter_map(|(i, framework)| {
484 if framework.trim().is_empty() {
485 Some(format!("Framework {} cannot be empty", i + 1))
486 } else if framework.len() > 100 {
487 Some(format!(
488 "Framework '{}' cannot exceed 100 characters",
489 framework
490 ))
491 } else {
492 None
493 }
494 }),
495 );
496
497 errors.extend(
499 tech_stack
500 .databases
501 .iter()
502 .enumerate()
503 .filter_map(|(i, database)| {
504 if database.trim().is_empty() {
505 Some(format!("Database {} cannot be empty", i + 1))
506 } else if database.len() > 100 {
507 Some(format!(
508 "Database '{}' cannot exceed 100 characters",
509 database
510 ))
511 } else {
512 None
513 }
514 }),
515 );
516
517 errors.extend(tech_stack.tools.iter().enumerate().filter_map(|(i, tool)| {
519 if tool.trim().is_empty() {
520 Some(format!("Tool {} cannot be empty", i + 1))
521 } else if tool.len() > 100 {
522 Some(format!("Tool '{}' cannot exceed 100 characters", tool))
523 } else {
524 None
525 }
526 }));
527
528 errors.extend(
530 tech_stack
531 .deployment
532 .iter()
533 .enumerate()
534 .filter_map(|(i, deployment)| {
535 if deployment.trim().is_empty() {
536 Some(format!("Deployment {} cannot be empty", i + 1))
537 } else if deployment.len() > 100 {
538 Some(format!(
539 "Deployment '{}' cannot exceed 100 characters",
540 deployment
541 ))
542 } else {
543 None
544 }
545 }),
546 );
547
548 if errors.is_empty() {
549 Ok(())
550 } else {
551 Err(errors)
552 }
553}
554
555pub fn validate_vision(vision: &Vision) -> Result<(), Vec<String>> {
557 let mut errors = Vec::new();
558
559 if vision.overview.trim().is_empty() {
561 errors.push("Vision overview cannot be empty".to_string());
562 }
563
564 if vision.overview.len() > 2000 {
565 errors.push("Vision overview cannot exceed 2000 characters".to_string());
566 }
567
568 if vision.goals.is_empty() {
570 errors.push("At least one goal must be specified".to_string());
571 }
572
573 errors.extend(vision.goals.iter().enumerate().filter_map(|(i, goal)| {
574 if goal.trim().is_empty() {
575 Some(format!("Goal {} cannot be empty", i + 1))
576 } else if goal.len() > 500 {
577 Some(format!("Goal '{}' cannot exceed 500 characters", goal))
578 } else {
579 None
580 }
581 }));
582
583 if vision.target_users.is_empty() {
585 errors.push("At least one target user must be specified".to_string());
586 }
587
588 errors.extend(
589 vision
590 .target_users
591 .iter()
592 .enumerate()
593 .filter_map(|(i, user)| {
594 if user.trim().is_empty() {
595 Some(format!("Target user {} cannot be empty", i + 1))
596 } else if user.len() > 200 {
597 Some(format!(
598 "Target user '{}' cannot exceed 200 characters",
599 user
600 ))
601 } else {
602 None
603 }
604 }),
605 );
606
607 if vision.success_criteria.is_empty() {
609 errors.push("At least one success criterion must be specified".to_string());
610 }
611
612 errors.extend(
613 vision
614 .success_criteria
615 .iter()
616 .enumerate()
617 .filter_map(|(i, criterion)| {
618 if criterion.trim().is_empty() {
619 Some(format!("Success criterion {} cannot be empty", i + 1))
620 } else if criterion.len() > 500 {
621 Some(format!(
622 "Success criterion '{}' cannot exceed 500 characters",
623 criterion
624 ))
625 } else {
626 None
627 }
628 }),
629 );
630
631 if errors.is_empty() {
632 Ok(())
633 } else {
634 Err(errors)
635 }
636}
637
638pub fn validate_specification(spec: &Specification) -> Result<(), Vec<String>> {
640 let mut errors = Vec::new();
641
642 if let Err(e) = crate::utils::id_generation::validate_spec_id(&spec.id) {
644 errors.push(format!("Specification ID: {}", e));
645 }
646
647 if spec.name.trim().is_empty() {
649 errors.push("Specification name cannot be empty".to_string());
650 }
651
652 if spec.name.len() > 100 {
653 errors.push("Specification name cannot exceed 100 characters".to_string());
654 }
655
656 if spec.description.trim().is_empty() {
658 errors.push("Specification description cannot be empty".to_string());
659 }
660
661 if spec.description.len() > 1000 {
662 errors.push("Specification description cannot exceed 1000 characters".to_string());
663 }
664
665 if spec.created_at > spec.updated_at {
667 errors.push("Created timestamp cannot be after updated timestamp".to_string());
668 }
669
670 if spec.content.len() > 10000 {
672 errors.push("Specification content cannot exceed 10000 characters".to_string());
673 }
674
675 if errors.is_empty() {
676 Ok(())
677 } else {
678 Err(errors)
679 }
680}
681
682pub fn validate_task(task: &Task) -> Result<(), Vec<String>> {
684 let mut errors = Vec::new();
685
686 if task.id.trim().is_empty() {
688 errors.push("Task ID cannot be empty".to_string());
689 }
690
691 if task.id.len() > 100 {
692 errors.push("Task ID cannot exceed 100 characters".to_string());
693 }
694
695 if task.title.trim().is_empty() {
697 errors.push("Task title cannot be empty".to_string());
698 }
699
700 if task.title.len() > 200 {
701 errors.push("Task title cannot exceed 200 characters".to_string());
702 }
703
704 if task.description.len() > 1000 {
706 errors.push("Task description cannot exceed 1000 characters".to_string());
707 }
708
709 if task.created_at > task.updated_at {
711 errors.push("Created timestamp cannot be after updated timestamp".to_string());
712 }
713
714 errors.extend(
716 task.dependencies
717 .iter()
718 .enumerate()
719 .filter_map(|(i, dependency)| {
720 if dependency.trim().is_empty() {
721 Some(format!("Dependency {} cannot be empty", i + 1))
722 } else if dependency.len() > 100 {
723 Some(format!(
724 "Dependency '{}' cannot exceed 100 characters",
725 dependency
726 ))
727 } else {
728 None
729 }
730 }),
731 );
732
733 if errors.is_empty() {
734 Ok(())
735 } else {
736 Err(errors)
737 }
738}
739
740pub fn validate_task_list(task_list: &TaskList) -> Result<(), Vec<String>> {
742 let mut errors = Vec::new();
743
744 errors.extend(task_list.tasks.iter().enumerate().flat_map(|(i, task)| {
746 validate_task(task)
747 .map(|_| Vec::<String>::new())
748 .unwrap_or_else(|task_errors| {
749 task_errors
750 .into_iter()
751 .map(|task_error| format!("Task {}: {}", i + 1, task_error))
752 .collect::<Vec<_>>()
753 })
754 }));
755
756 let now = Utc::now();
758 if task_list.last_updated > now {
759 errors.push("Last updated timestamp cannot be in the future".to_string());
760 }
761
762 if errors.is_empty() {
763 Ok(())
764 } else {
765 Err(errors)
766 }
767}
768
769pub fn validate_project_name_availability(
771 project_name: &str,
772 existing_projects: &[String],
773) -> Result<(), String> {
774 if existing_projects.iter().any(|name| name == project_name) {
775 Err(format!("Project name '{}' is already taken", project_name))
776 } else {
777 Ok(())
778 }
779}
780
781pub fn validate_spec_name_availability(
783 spec_name: &str,
784 existing_specs: &[String],
785) -> Result<(), String> {
786 if existing_specs.iter().any(|name| name == spec_name) {
787 Err(format!(
788 "Specification name '{}' is already taken in this project",
789 spec_name
790 ))
791 } else {
792 Ok(())
793 }
794}
795
796pub fn validate_file_path_safety(path: &str) -> Result<(), String> {
798 if path.contains("..") {
799 return Err("Path contains directory traversal attempt".to_string());
800 }
801
802 if path.starts_with('/') || path.starts_with('\\') {
803 return Err("Path cannot be absolute".to_string());
804 }
805
806 if path.contains('\0') {
807 return Err("Path contains null character".to_string());
808 }
809
810 let dangerous_patterns = ["/etc/", "/var/", "/usr/", "/bin/", "/sbin/", "C:\\", "D:\\"];
812 if dangerous_patterns
813 .iter()
814 .any(|pattern| path.to_lowercase().contains(pattern))
815 {
816 return Err(format!(
817 "Path contains potentially dangerous pattern: {}",
818 dangerous_patterns
819 .iter()
820 .find(|pattern| path.to_lowercase().contains(*pattern))
821 .unwrap()
822 ));
823 }
824
825 Ok(())
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use crate::models::SpecStatus;
832
833 fn create_test_project() -> Project {
834 Project {
835 name: "test_project".to_string(),
836 description: "A test project".to_string(),
837 created_at: Utc::now(),
838 updated_at: Utc::now(),
839 tech_stack: TechStack {
840 languages: vec!["Rust".to_string()],
841 frameworks: vec!["Actix".to_string()],
842 databases: vec!["PostgreSQL".to_string()],
843 tools: vec!["Cargo".to_string()],
844 deployment: vec!["Docker".to_string()],
845 },
846 vision: Vision {
847 overview: "A test project overview".to_string(),
848 goals: vec!["Goal 1".to_string()],
849 target_users: vec!["Developer".to_string()],
850 success_criteria: vec!["Criterion 1".to_string()],
851 },
852 }
853 }
854
855 fn create_test_spec() -> Specification {
856 Specification {
857 id: "20240101_test_spec".to_string(),
858 name: "test_spec".to_string(),
859 description: "A test specification".to_string(),
860 status: SpecStatus::Draft,
861 created_at: Utc::now(),
862 updated_at: Utc::now(),
863 content: "Test content".to_string(),
864 }
865 }
866
867 fn create_test_task() -> Task {
868 Task {
869 id: "task_1".to_string(),
870 title: "Test Task".to_string(),
871 description: "A test task".to_string(),
872 status: crate::models::TaskStatus::Todo,
873 priority: crate::models::TaskPriority::Medium,
874 dependencies: vec![],
875 created_at: Utc::now(),
876 updated_at: Utc::now(),
877 }
878 }
879
880 #[test]
881 fn test_validate_project() {
882 let project = create_test_project();
883 assert!(validate_project(&project).is_ok());
884 }
885
886 #[test]
887 fn test_validate_specification() {
888 let spec = create_test_spec();
889 assert!(validate_specification(&spec).is_ok());
890 }
891
892 #[test]
893 fn test_validate_task() {
894 let task = create_test_task();
895 assert!(validate_task(&task).is_ok());
896 }
897
898 #[test]
899 fn test_validate_project_name_availability() {
900 let existing = vec!["project1".to_string(), "project2".to_string()];
901
902 assert!(validate_project_name_availability("new_project", &existing).is_ok());
903 assert!(validate_project_name_availability("project1", &existing).is_err());
904 }
905
906 #[test]
907 fn test_validate_file_path_safety() {
908 assert!(validate_file_path_safety("valid/path").is_ok());
909 assert!(validate_file_path_safety("../dangerous").is_err());
910 assert!(validate_file_path_safety("/absolute/path").is_err());
911 assert!(validate_file_path_safety("path\0with\0nulls").is_err());
912 }
913}