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