mockforge_ftp/
fixtures.rs1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use crate::GenerationPattern;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FtpFixture {
10 pub identifier: String,
11 pub name: String,
12 pub description: Option<String>,
13 pub virtual_files: Vec<VirtualFileConfig>,
14 pub upload_rules: Vec<UploadRule>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct VirtualFileConfig {
20 pub path: PathBuf,
21 pub content: FileContentConfig,
22 pub permissions: String,
23 pub owner: String,
24 pub group: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "type")]
30pub enum FileContentConfig {
31 #[serde(rename = "static")]
32 Static { content: String },
33 #[serde(rename = "template")]
34 Template { template: String },
35 #[serde(rename = "generated")]
36 Generated {
37 size: usize,
38 pattern: GenerationPattern,
39 },
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct UploadRule {
45 pub path_pattern: String,
46 pub auto_accept: bool,
47 pub validation: Option<FileValidation>,
48 pub storage: UploadStorage,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FileValidation {
54 pub max_size_bytes: Option<u64>,
55 pub allowed_extensions: Option<Vec<String>>,
56 pub mime_types: Option<Vec<String>>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(tag = "type")]
62pub enum UploadStorage {
63 #[serde(rename = "discard")]
64 Discard,
65 #[serde(rename = "memory")]
66 Memory,
67 #[serde(rename = "file")]
68 File { path: PathBuf },
69}
70
71impl VirtualFileConfig {
72 pub fn to_file_fixture(self) -> crate::vfs::FileFixture {
73 let content = match self.content {
74 FileContentConfig::Static { content } => {
75 crate::vfs::FileContent::Static(content.into_bytes())
76 }
77 FileContentConfig::Template { template } => crate::vfs::FileContent::Template(template),
78 FileContentConfig::Generated { size, pattern } => {
79 crate::vfs::FileContent::Generated { size, pattern }
80 }
81 };
82
83 let metadata = crate::vfs::FileMetadata {
84 permissions: self.permissions,
85 owner: self.owner,
86 group: self.group,
87 size: 0, };
89
90 crate::vfs::FileFixture {
91 path: self.path,
92 content,
93 metadata,
94 }
95 }
96}
97
98impl UploadRule {
99 pub fn matches_path(&self, path: &str) -> bool {
100 match Regex::new(&self.path_pattern) {
101 Ok(regex) => regex.is_match(path),
102 Err(_) => false,
103 }
104 }
105
106 pub fn validate_file(&self, data: &[u8], filename: &str) -> Result<(), String> {
107 if let Some(validation) = &self.validation {
108 if let Some(max_size) = validation.max_size_bytes {
110 if data.len() as u64 > max_size {
111 return Err(format!(
112 "File too large: {} bytes (max: {})",
113 data.len(),
114 max_size
115 ));
116 }
117 }
118
119 if let Some(extensions) = &validation.allowed_extensions {
121 let has_valid_ext = extensions.iter().any(|ext| {
122 filename.to_lowercase().ends_with(&format!(".{}", ext.to_lowercase()))
123 });
124 if !extensions.is_empty() && !has_valid_ext {
125 return Err(format!("Invalid file extension. Allowed: {:?}", extensions));
126 }
127 }
128
129 if let Some(mime_types) = &validation.mime_types {
131 let guessed = mime_guess::from_path(filename).first_or_octet_stream();
132 let guessed_str = guessed.as_ref();
133 if !mime_types.iter().any(|allowed| allowed == guessed_str) {
134 return Err(format!(
135 "Invalid MIME type: {} (allowed: {:?})",
136 guessed_str, mime_types
137 ));
138 }
139 }
140 }
141
142 Ok(())
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_validate_file_mime_type() {
152 let validation = FileValidation {
153 max_size_bytes: None,
154 allowed_extensions: None,
155 mime_types: Some(vec!["text/plain".to_string(), "application/json".to_string()]),
156 };
157
158 let rule = UploadRule {
159 path_pattern: ".*".to_string(),
160 auto_accept: true,
161 validation: Some(validation),
162 storage: UploadStorage::Discard,
163 };
164
165 let data = b"Hello world";
167 let filename = "test.txt";
168 assert!(rule.validate_file(data, filename).is_ok());
169
170 let filename_invalid = "test.png";
174 assert!(rule.validate_file(data, filename_invalid).is_err());
175
176 let rule_no_mime = UploadRule {
178 path_pattern: ".*".to_string(),
179 auto_accept: true,
180 validation: Some(FileValidation {
181 max_size_bytes: None,
182 allowed_extensions: None,
183 mime_types: None,
184 }),
185 storage: UploadStorage::Discard,
186 };
187 assert!(rule_no_mime.validate_file(data, filename).is_ok());
188 }
189}