polykit_core/
command_validator.rs1use crate::error::{Error, Result};
4
5pub struct CommandValidator {
10 allow_shell: bool,
11}
12
13impl Default for CommandValidator {
14 fn default() -> Self {
15 Self { allow_shell: true }
16 }
17}
18
19impl CommandValidator {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn strict() -> Self {
27 Self { allow_shell: false }
28 }
29
30 pub fn validate(&self, command: &str) -> Result<()> {
36 if command.trim().is_empty() {
37 return Err(Error::TaskExecution {
38 package: "unknown".to_string(),
39 task: "unknown".to_string(),
40 message: "Command cannot be empty".to_string(),
41 });
42 }
43
44 if command.len() > 10_000 {
45 return Err(Error::TaskExecution {
46 package: "unknown".to_string(),
47 task: "unknown".to_string(),
48 message: "Command exceeds maximum length of 10,000 characters".to_string(),
49 });
50 }
51
52 if command.contains('\0') {
53 return Err(Error::TaskExecution {
54 package: "unknown".to_string(),
55 task: "unknown".to_string(),
56 message: "Command contains null bytes".to_string(),
57 });
58 }
59
60 if command.contains("\r\n") || command.contains('\n') {
61 let trimmed = command.trim();
62 if trimmed.contains('\n') {
63 return Err(Error::TaskExecution {
64 package: "unknown".to_string(),
65 task: "unknown".to_string(),
66 message: "Command contains embedded newlines".to_string(),
67 });
68 }
69 }
70
71 if !self.allow_shell
72 && (command.contains(';')
73 || command.contains("&&")
74 || command.contains("||")
75 || command.contains('|')
76 || command.contains('`')
77 || command.contains('$'))
78 {
79 return Err(Error::TaskExecution {
80 package: "unknown".to_string(),
81 task: "unknown".to_string(),
82 message: "Command contains shell features that are not allowed in strict mode"
83 .to_string(),
84 });
85 }
86
87 Ok(())
88 }
89
90 pub fn validate_identifier(identifier: &str, context: &str) -> Result<()> {
94 if identifier.is_empty() {
95 return Err(Error::InvalidPackageName(format!(
96 "{} cannot be empty",
97 context
98 )));
99 }
100
101 if identifier.len() > 255 {
102 return Err(Error::InvalidPackageName(format!(
103 "{} exceeds maximum length of 255 characters",
104 context
105 )));
106 }
107
108 if identifier.starts_with('.') || identifier.starts_with('-') {
109 return Err(Error::InvalidPackageName(format!(
110 "{} cannot start with '.' or '-'",
111 context
112 )));
113 }
114
115 if identifier.contains("..") {
116 return Err(Error::InvalidPackageName(format!(
117 "{} cannot contain '..'",
118 context
119 )));
120 }
121
122 if identifier.contains('/') || identifier.contains('\\') {
123 return Err(Error::InvalidPackageName(format!(
124 "{} cannot contain path separators",
125 context
126 )));
127 }
128
129 for ch in identifier.chars() {
130 if !ch.is_alphanumeric() && ch != '-' && ch != '_' && ch != '.' && ch != '@' {
131 return Err(Error::InvalidPackageName(format!(
132 "{} contains invalid character: '{}'",
133 context, ch
134 )));
135 }
136 }
137
138 Ok(())
139 }
140}