Skip to main content

orion_accessor/addr/
validation.rs

1//! 地址配置验证模块
2//!
3//! 提供地址配置验证功能,确保地址格式正确且可访问
4
5use crate::addr::constants;
6//use crate::prelude::*;
7use std::path::Path;
8use url::Url;
9
10use super::{Address, GitRepository, HttpResource, LocalPath};
11
12/// 地址验证结果
13pub type ValidationResult = Result<(), Vec<ValidationError>>;
14
15/// 验证错误类型
16#[derive(Debug, Clone, PartialEq)]
17pub struct ValidationError {
18    /// 错误字段
19    pub field: String,
20    /// 错误描述
21    pub message: String,
22    /// 错误代码
23    pub code: String,
24}
25
26impl ValidationError {
27    pub fn new(field: &str, message: &str, code: &str) -> Self {
28        Self {
29            field: field.to_string(),
30            message: message.to_string(),
31            code: code.to_string(),
32        }
33    }
34}
35
36/// 地址验证trait
37pub trait Validate {
38    /// 验证地址配置
39    fn validate(&self) -> ValidationResult;
40
41    /// 验证地址是否可访问
42    fn is_accessible(&self) -> bool;
43}
44
45impl Validate for Address {
46    fn validate(&self) -> ValidationResult {
47        match self {
48            Address::Git(repo) => repo.validate(),
49            Address::Http(resource) => resource.validate(),
50            Address::Local(path) => path.validate(),
51        }
52    }
53
54    fn is_accessible(&self) -> bool {
55        match self {
56            Address::Git(repo) => repo.is_accessible(),
57            Address::Http(resource) => resource.is_accessible(),
58            Address::Local(path) => path.is_accessible(),
59        }
60    }
61}
62
63impl Validate for GitRepository {
64    fn validate(&self) -> ValidationResult {
65        let mut errors = Vec::new();
66
67        // 验证仓库地址
68        if self.repo().is_empty() {
69            errors.push(ValidationError::new(
70                "repo",
71                "仓库地址不能为空",
72                "EMPTY_REPO",
73            ));
74        } else if !is_valid_git_url(self.repo()) {
75            errors.push(ValidationError::new(
76                "repo",
77                "无效的Git仓库地址格式",
78                "INVALID_GIT_URL",
79            ));
80        }
81
82        // 验证SSH密钥路径
83        if let Some(ssh_key) = &self.ssh_key()
84            && !Path::new(ssh_key).exists()
85        {
86            errors.push(ValidationError::new(
87                "ssh_key",
88                &format!("SSH密钥文件不存在: {ssh_key}",),
89                "SSH_KEY_NOT_FOUND",
90            ));
91        }
92
93        // 验证认证配置
94        if self.token().is_some() && self.username().is_none() {
95            errors.push(ValidationError::new(
96                "username",
97                "使用Token认证时必须提供用户名",
98                "MISSING_USERNAME",
99            ));
100        }
101
102        // 验证版本标识符
103        let version_count = [
104            self.tag().as_ref(),
105            self.branch().as_ref(),
106            self.rev().as_ref(),
107        ]
108        .iter()
109        .filter(|x| x.is_some())
110        .count();
111
112        if version_count > 1 {
113            errors.push(ValidationError::new(
114                "version",
115                "不能同时指定tag、branch和rev中的多个",
116                "CONFLICTING_VERSIONS",
117            ));
118        }
119
120        if errors.is_empty() {
121            Ok(())
122        } else {
123            Err(errors)
124        }
125    }
126
127    fn is_accessible(&self) -> bool {
128        // 简化的可访问性检查
129        // 实际实现可能需要网络连接测试
130        is_valid_git_url(self.repo())
131    }
132}
133
134impl Validate for HttpResource {
135    fn validate(&self) -> ValidationResult {
136        let mut errors = Vec::new();
137
138        // 验证URL格式
139        if self.url().is_empty() {
140            errors.push(ValidationError::new("url", "URL不能为空", "EMPTY_URL"));
141        } else if let Err(e) = Url::parse(self.url()) {
142            errors.push(ValidationError::new(
143                "url",
144                &format!("无效的URL格式: {e}"),
145                "INVALID_URL",
146            ));
147        }
148
149        // 验证认证信息
150        if self.username().is_some() && self.password().is_none() {
151            errors.push(ValidationError::new(
152                "password",
153                "提供用户名时必须提供密码",
154                "MISSING_PASSWORD",
155            ));
156        }
157
158        if errors.is_empty() {
159            Ok(())
160        } else {
161            Err(errors)
162        }
163    }
164
165    fn is_accessible(&self) -> bool {
166        // 简化的可访问性检查
167        Url::parse(self.url()).is_ok()
168    }
169}
170
171impl Validate for LocalPath {
172    fn validate(&self) -> ValidationResult {
173        let mut errors = Vec::new();
174
175        // 验证路径格式
176        let path_str = self.path();
177        if path_str.is_empty() {
178            errors.push(ValidationError::new(
179                "path",
180                "本地路径不能为空",
181                "EMPTY_PATH",
182            ));
183        } else {
184            let path = Path::new(path_str);
185
186            // 检查路径是否包含非法字符
187            if path_str.contains("\\") && cfg!(not(target_os = "windows")) {
188                errors.push(ValidationError::new(
189                    "path",
190                    "在非Windows系统上使用了反斜杠路径分隔符",
191                    "INVALID_PATH_SEPARATOR",
192                ));
193            }
194
195            // 检查相对路径
196            if path.is_relative() && !path_str.starts_with("./") && !path_str.starts_with("../") {
197                errors.push(ValidationError::new(
198                    "path",
199                    "相对路径应以./或../开头",
200                    "INVALID_RELATIVE_PATH",
201                ));
202            }
203        }
204
205        if errors.is_empty() {
206            Ok(())
207        } else {
208            Err(errors)
209        }
210    }
211
212    fn is_accessible(&self) -> bool {
213        Path::new(self.path()).exists()
214    }
215}
216
217/// 验证Git URL格式
218fn is_valid_git_url(url: &str) -> bool {
219    // HTTPS格式
220    if url.starts_with(constants::git::HTTPS_PREFIX) && url.ends_with(".git") {
221        return Url::parse(url).is_ok();
222    }
223
224    // SSH格式 (git@host:repo.git)
225    if url.starts_with(constants::git::SSH_PREFIX) && url.contains(':') && url.ends_with(".git") {
226        return true;
227    }
228
229    // Git协议格式
230    if url.starts_with(constants::git::GIT_PROTOCOL) && url.ends_with(".git") {
231        return Url::parse(url).is_ok();
232    }
233
234    // 简化的GitHub/GitLab等格式
235    url.contains("github.com") || url.contains("gitlab.com") || url.contains("gitea.com")
236}
237
238/// 批量验证多个地址
239pub fn validate_addresses(addresses: &[Address]) -> ValidationResult {
240    let mut all_errors = Vec::new();
241
242    for (index, addr) in addresses.iter().enumerate() {
243        if let Err(errors) = addr.validate() {
244            for error in errors {
245                all_errors.push(ValidationError::new(
246                    &format!("address[{}].{}", index, error.field),
247                    &error.message,
248                    &error.code,
249                ));
250            }
251        }
252    }
253
254    if all_errors.is_empty() {
255        Ok(())
256    } else {
257        Err(all_errors)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_git_repository_validation() {
267        let repo = GitRepository::from("https://github.com/user/repo.git");
268        assert!(repo.validate().is_ok());
269
270        let invalid_repo = GitRepository::from("");
271        assert!(invalid_repo.validate().is_err());
272
273        let ssh_repo = GitRepository::from("git@github.com:user/repo.git");
274        assert!(ssh_repo.validate().is_ok());
275    }
276
277    #[test]
278    fn test_http_resource_validation() {
279        let resource = HttpResource::from("https://example.com/file.zip");
280        assert!(resource.validate().is_ok());
281
282        let invalid_resource = HttpResource::from("invalid-url");
283        assert!(invalid_resource.validate().is_err());
284
285        let empty_resource = HttpResource::from("");
286        assert!(empty_resource.validate().is_err());
287    }
288
289    #[test]
290    fn test_local_path_validation() {
291        let path = LocalPath::from("./relative/path");
292        assert!(path.validate().is_ok());
293
294        let absolute_path = LocalPath::from("/absolute/path");
295        assert!(absolute_path.validate().is_ok());
296
297        let invalid_path = LocalPath::from("");
298        assert!(invalid_path.validate().is_err());
299    }
300
301    #[test]
302    fn test_address_validation() {
303        let git_addr = Address::Git(GitRepository::from("https://github.com/user/repo.git"));
304        assert!(git_addr.validate().is_ok());
305
306        let http_addr = Address::Http(HttpResource::from("https://example.com/file.zip"));
307        assert!(http_addr.validate().is_ok());
308
309        let local_addr = Address::Local(LocalPath::from("./path"));
310        assert!(local_addr.validate().is_ok());
311    }
312
313    #[test]
314    fn test_batch_validation() {
315        let addresses = vec![
316            Address::Git(GitRepository::from("https://github.com/user/repo.git")),
317            Address::Http(HttpResource::from("https://example.com/file.zip")),
318        ];
319
320        assert!(validate_addresses(&addresses).is_ok());
321
322        let invalid_addresses = vec![
323            Address::Git(GitRepository::from("")),
324            Address::Http(HttpResource::from("invalid-url")),
325        ];
326
327        assert!(validate_addresses(&invalid_addresses).is_err());
328    }
329
330    #[test]
331    fn test_is_valid_git_url() {
332        assert!(is_valid_git_url("https://github.com/user/repo.git"));
333        assert!(is_valid_git_url("git@github.com:user/repo.git"));
334        assert!(is_valid_git_url("git://github.com/user/repo.git"));
335        assert!(is_valid_git_url("https://gitlab.com/user/repo.git"));
336        assert!(!is_valid_git_url("invalid-url"));
337        assert!(!is_valid_git_url(""));
338    }
339
340    #[test]
341    fn test_git_repository_authentication_and_version_conflicts() {
342        // Test GitRepository with valid SSH key path (mocked test since we can't create files)
343        let repo_with_ssh_key = GitRepository::from("https://github.com/user/repo.git")
344            .with_ssh_key("/path/to/ssh/key");
345
346        // SSH key validation will check if file exists - in test environment it will fail
347        // but we can test that the validation logic runs
348        let result = repo_with_ssh_key.validate();
349        assert!(result.is_err() || result.is_ok()); // Either is acceptable in test
350
351        // Test token authentication without username (should fail)
352        let repo_with_token_only =
353            GitRepository::from("https://github.com/user/repo.git").with_token("test-token");
354        let result = repo_with_token_only.validate();
355        assert!(result.is_err());
356        if let Err(errors) = result {
357            assert!(errors.iter().any(|e| e.code == "MISSING_USERNAME"));
358        }
359
360        // Test token authentication with username (should pass)
361        let repo_with_token_and_username = GitRepository::from("https://github.com/user/repo.git")
362            .with_token("test-token")
363            .with_username("test-user");
364        assert!(repo_with_token_and_username.validate().is_ok());
365
366        // Test version identifier conflicts (tag and branch together)
367        let repo_with_conflicting_versions =
368            GitRepository::from("https://github.com/user/repo.git")
369                .with_tag("v1.0")
370                .with_branch("main");
371        let result = repo_with_conflicting_versions.validate();
372        assert!(result.is_err());
373        if let Err(errors) = result {
374            assert!(errors.iter().any(|e| e.code == "CONFLICTING_VERSIONS"));
375        }
376
377        // Test valid single version identifier
378        let repo_with_valid_tag =
379            GitRepository::from("https://github.com/user/repo.git").with_tag("v1.0");
380        assert!(repo_with_valid_tag.validate().is_ok());
381
382        let repo_with_valid_branch =
383            GitRepository::from("https://github.com/user/repo.git").with_branch("main");
384        assert!(repo_with_valid_branch.validate().is_ok());
385
386        let repo_with_valid_rev =
387            GitRepository::from("https://github.com/user/repo.git").with_rev("abc123");
388        assert!(repo_with_valid_rev.validate().is_ok());
389    }
390
391    #[test]
392    fn test_http_resource_authentication_edge_cases() {
393        // Test HTTP resource with valid credentials
394        let valid_http = HttpResource::from("https://example.com/file.zip")
395            .with_credentials("username", "password");
396        assert!(valid_http.validate().is_ok());
397
398        // Test HTTP resource with username but no password
399        let mut http_with_username_only = HttpResource::from("https://example.com/file.zip");
400        // Set username without password using setter method
401        http_with_username_only.set_username(Some("username".to_string()));
402        let result = http_with_username_only.validate();
403        assert!(result.is_err());
404        if let Err(errors) = result {
405            assert!(errors.iter().any(|e| e.code == "MISSING_PASSWORD"));
406        }
407
408        // Test various invalid URL formats (only those that fail to parse)
409        let invalid_urls = vec!["://missing-protocol", "http//missing-colon.com"];
410
411        for url in invalid_urls {
412            let resource = HttpResource::from(url);
413            let result = resource.validate();
414            assert!(result.is_err(), "URL {url} should be invalid");
415        }
416
417        // Test URLs that parse as valid but aren't HTTP/HTTPS
418        // Current validation only checks if URL can be parsed, not the scheme
419        let valid_but_non_http_urls = vec!["ftp://unsupported-protocol.com"];
420
421        for url in valid_but_non_http_urls {
422            let resource = HttpResource::from(url);
423            // These will pass validation because they parse as valid URLs
424            // even though they're not HTTP/HTTPS
425            assert!(
426                resource.validate().is_ok(),
427                "URL {url} should pass validation (parses as valid)",
428            );
429        }
430
431        // Test URL parsing edge cases
432        let edge_case_urls = vec![
433            "https://example.com/",
434            "https://example.com/path/",
435            "https://example.com/path/to/file.txt",
436            "https://example.com/path/to/file.txt?param=value",
437            "https://example.com/path/to/file.txt#fragment",
438        ];
439
440        for url in edge_case_urls {
441            let resource = HttpResource::from(url);
442            assert!(resource.validate().is_ok(), "URL {url} should be valid");
443        }
444    }
445
446    #[test]
447    fn test_local_path_advanced_validation() {
448        // Test Windows path separator on non-Windows systems
449        let windows_style_path = LocalPath::from("C:\\windows\\path");
450        if cfg!(not(target_os = "windows")) {
451            let result = windows_style_path.validate();
452            assert!(result.is_err());
453            if let Err(errors) = result {
454                assert!(errors.iter().any(|e| e.code == "INVALID_PATH_SEPARATOR"));
455            }
456        }
457
458        // Test invalid relative paths (missing ./ or ../ prefix)
459        let invalid_relative_path = LocalPath::from("relative/path");
460        let result = invalid_relative_path.validate();
461        assert!(result.is_err());
462        if let Err(errors) = result {
463            assert!(errors.iter().any(|e| e.code == "INVALID_RELATIVE_PATH"));
464        }
465
466        // Test valid relative paths with different prefixes
467        let valid_relative_paths = vec![
468            "./relative/path",
469            "./relative/path/file.txt",
470            "../relative/path",
471            "../relative/path/file.txt",
472            "./",
473            "../",
474        ];
475
476        for path in valid_relative_paths {
477            let local_path = LocalPath::from(path);
478            assert!(local_path.validate().is_ok(), "Path {path} should be valid",);
479        }
480
481        // Test absolute paths on different systems
482        let absolute_paths = if cfg!(target_os = "windows") {
483            vec![
484                "C:\\absolute\\path",
485                "C:\\absolute\\path\\file.txt",
486                "\\\\network\\path",
487            ]
488        } else {
489            vec!["/absolute/path", "/absolute/path/file.txt", "/absolute"]
490        };
491
492        for path in absolute_paths {
493            let local_path = LocalPath::from(path);
494            assert!(local_path.validate().is_ok(), "Path {path} should be valid",);
495        }
496
497        // Test empty path
498        let empty_path = LocalPath::from("");
499        let result = empty_path.validate();
500        assert!(result.is_err());
501        if let Err(errors) = result {
502            assert!(errors.iter().any(|e| e.code == "EMPTY_PATH"));
503        }
504    }
505}