forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate.rs
More file actions
74 lines (68 loc) · 2.97 KB
/
Copy pathvalidate.rs
File metadata and controls
74 lines (68 loc) · 2.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//! Input validation for Tauri commands exposed to the WebView.
//! Untrusted data (from a possible XSS) must not reach the filesystem or network unchecked.
use std::path::Path;
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
const ALLOWED_HOSTS: &[&str] = &[
"huggingface.co",
"hf.co",
"cdn-lfs.huggingface.co",
"cdn-lfs.hf.co",
];
pub fn validate_filename(name: &str) -> Result<&str, String> {
if name.is_empty() || name.len() > 256 {
return Err("filename length out of range".into());
}
if name.contains('/') || name.contains('\\') || name.contains("..") || name.contains('\0') {
return Err("filename contains forbidden characters".into());
}
let p = Path::new(name);
match p.extension().and_then(|e| e.to_str()) {
Some("gguf") | Some("onnx") => Ok(name),
_ => Err("unsupported file extension".into()),
}
}
/// Defence-in-depth guard for arbitrary text passed to Tauri commands.
/// Bounds the size and refuses null bytes — anything more specific
/// (charset, extension, …) should use a dedicated validator.
pub fn validate_bounded_text(text: &str, max_bytes: usize, label: &str) -> Result<(), String> {
if text.len() > max_bytes {
return Err(format!("{label} exceeds {max_bytes} byte limit"));
}
if text.contains('\0') {
return Err(format!("{label} contains a null byte"));
}
Ok(())
}
/// Guard for user-supplied asset names that will be concatenated into a
/// filesystem path (voice clones, etc). Refuses path separators, traversal
/// components, control chars, and caps the length.
pub fn validate_voice_clone_name(name: &str) -> Result<&str, String> {
if name.is_empty() || name.len() > 128 {
return Err("voice clone name length out of range".into());
}
if name.contains('/') || name.contains('\\') || name.contains("..") || name.contains('\0') {
return Err("voice clone name contains forbidden characters".into());
}
let first = name.chars().next().ok_or("empty")?;
if !(first.is_ascii_alphanumeric() || first == '_') {
return Err("voice clone name must start with a letter, digit, or underscore".into());
}
if !name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ' ')) {
return Err("voice clone name has invalid characters".into());
}
Ok(name)
}
// `validate_url` pulls in the `url` crate which is in android target-deps.
// Also compiled for test builds so host-machine unit tests can reach it.
#[cfg(any(target_os = "android", test))]
pub fn validate_url(url: &str) -> Result<&str, String> {
let parsed = url::Url::parse(url).map_err(|e| format!("invalid url: {e}"))?;
if parsed.scheme() != "https" {
return Err("only https URLs allowed".into());
}
let host = parsed.host_str().ok_or("missing host")?;
if !ALLOWED_HOSTS.iter().any(|h| host == *h || host.ends_with(&format!(".{h}"))) {
return Err(format!("host not in allowlist: {host}"));
}
Ok(url)
}