quack_rs/validate/
function_name.rs1use crate::error::ExtensionError;
13
14const MAX_FUNCTION_NAME_LEN: usize = 256;
19
20pub fn validate_function_name(name: &str) -> Result<(), ExtensionError> {
52 if name.is_empty() {
53 return Err(ExtensionError::new("function name must not be empty"));
54 }
55
56 if name.len() > MAX_FUNCTION_NAME_LEN {
57 return Err(ExtensionError::new(format!(
58 "function name must not exceed {MAX_FUNCTION_NAME_LEN} characters, got {}",
59 name.len()
60 )));
61 }
62
63 if name.bytes().any(|b| b == 0) {
65 return Err(ExtensionError::new(
66 "function name must not contain null bytes",
67 ));
68 }
69
70 let first = name.as_bytes()[0];
71 if !first.is_ascii_lowercase() && first != b'_' {
72 return Err(ExtensionError::new(format!(
73 "function name must start with a lowercase letter or underscore, got '{}'",
74 name.chars().next().unwrap_or('?')
75 )));
76 }
77
78 for (i, ch) in name.chars().enumerate() {
79 if !matches!(ch, 'a'..='z' | '0'..='9' | '_') {
80 return Err(ExtensionError::new(format!(
81 "function name contains invalid character '{ch}' at position {i}; \
82 only lowercase letters, digits, and underscores are allowed"
83 )));
84 }
85 }
86
87 Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn valid_simple() {
96 assert!(validate_function_name("word_count").is_ok());
97 }
98
99 #[test]
100 fn valid_with_digits() {
101 assert!(validate_function_name("my_func_v2").is_ok());
102 }
103
104 #[test]
105 fn valid_underscore_prefix() {
106 assert!(validate_function_name("_internal").is_ok());
107 }
108
109 #[test]
110 fn valid_single_char() {
111 assert!(validate_function_name("f").is_ok());
112 }
113
114 #[test]
115 fn empty_rejected() {
116 let err = validate_function_name("").unwrap_err();
117 assert!(err.as_str().contains("empty"));
118 }
119
120 #[test]
121 fn uppercase_rejected() {
122 let err = validate_function_name("MyFunc").unwrap_err();
123 assert!(err.as_str().contains("lowercase letter or underscore"));
124 }
125
126 #[test]
127 fn uppercase_mid_rejected() {
128 let err = validate_function_name("myFunc").unwrap_err();
129 assert!(err.as_str().contains("invalid character"));
130 }
131
132 #[test]
133 fn hyphen_rejected() {
134 let err = validate_function_name("my-func").unwrap_err();
135 assert!(err.as_str().contains("invalid character"));
136 }
137
138 #[test]
139 fn starts_with_digit_rejected() {
140 let err = validate_function_name("1func").unwrap_err();
141 assert!(err.as_str().contains("lowercase letter or underscore"));
142 }
143
144 #[test]
145 fn space_rejected() {
146 let err = validate_function_name("my func").unwrap_err();
147 assert!(err.as_str().contains("invalid character"));
148 }
149
150 #[test]
151 fn special_char_rejected() {
152 let err = validate_function_name("my@func").unwrap_err();
153 assert!(err.as_str().contains("invalid character"));
154 }
155
156 #[test]
157 fn null_byte_rejected() {
158 let err = validate_function_name("my\0func").unwrap_err();
159 assert!(err.as_str().contains("null bytes"));
160 }
161
162 #[test]
163 fn too_long_rejected() {
164 let long_name: String = "a".repeat(257);
165 let err = validate_function_name(&long_name).unwrap_err();
166 assert!(err.as_str().contains("256 characters"));
167 }
168
169 #[test]
170 fn max_length_accepted() {
171 let max_name: String = "a".repeat(256);
172 assert!(validate_function_name(&max_name).is_ok());
173 }
174
175 #[test]
176 fn semicolon_rejected() {
177 let err = validate_function_name("func;drop").unwrap_err();
178 assert!(err.as_str().contains("invalid character"));
179 }
180
181 #[test]
182 fn quote_rejected() {
183 let err = validate_function_name("func'name").unwrap_err();
184 assert!(err.as_str().contains("invalid character"));
185 }
186}