1extern crate alloc;
2
3use alloc::string::String;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7pub const CC_LB_PLUGIN_MAGIC: [u8; 8] = [0xCC, 0x1B, 0x70, 0x10, 0x00, 0x01, 0x00, 0x00];
9
10pub const CC_LB_PLUGIN_SECTION_NAME: &str = "cc_lb.plugin.v1";
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct PluginIdentity {
20 pub magic: [u8; 8],
22
23 pub abi_envelope: u32,
25
26 pub plugin_name: String,
28
29 pub plugin_version: String,
31}
32
33#[derive(Debug, Clone, Error, PartialEq, Eq)]
35pub enum IdentityError {
36 #[error("magic number mismatch")]
37 MagicMismatch,
38
39 #[error("plugin name is empty")]
40 PluginNameEmpty,
41
42 #[error("plugin name does not match pattern ^[a-z][a-z0-9_-]*$ (max 64 bytes)")]
43 PluginNameInvalid,
44
45 #[error("plugin version is empty")]
46 PluginVersionEmpty,
47
48 #[error("plugin version exceeds max length of 32 bytes")]
49 PluginVersionTooLong,
50}
51
52impl PluginIdentity {
53 pub fn validate(&self) -> Result<(), IdentityError> {
60 if self.magic != CC_LB_PLUGIN_MAGIC {
62 return Err(IdentityError::MagicMismatch);
63 }
64
65 if self.plugin_name.is_empty() {
67 return Err(IdentityError::PluginNameEmpty);
68 }
69
70 let name_bytes = self.plugin_name.as_bytes();
71 if name_bytes.len() > 64 {
72 return Err(IdentityError::PluginNameInvalid);
73 }
74
75 let is_valid_name = name_bytes[0].is_ascii_lowercase()
76 && name_bytes
77 .iter()
78 .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-');
79
80 if !is_valid_name {
81 return Err(IdentityError::PluginNameInvalid);
82 }
83
84 if self.plugin_version.is_empty() {
86 return Err(IdentityError::PluginVersionEmpty);
87 }
88
89 if self.plugin_version.len() > 32 {
90 return Err(IdentityError::PluginVersionTooLong);
91 }
92
93 Ok(())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use alloc::string::ToString;
101
102 #[test]
103 fn test_valid_identity() {
104 let identity = PluginIdentity {
105 magic: CC_LB_PLUGIN_MAGIC,
106 abi_envelope: 1,
107 plugin_name: "my-plugin".to_string(),
108 plugin_version: "1.0.0".to_string(),
109 };
110 assert!(identity.validate().is_ok());
111 }
112
113 #[test]
114 fn test_magic_mismatch() {
115 let identity = PluginIdentity {
116 magic: [0; 8],
117 abi_envelope: 1,
118 plugin_name: "my-plugin".to_string(),
119 plugin_version: "1.0.0".to_string(),
120 };
121 assert_eq!(identity.validate(), Err(IdentityError::MagicMismatch));
122 }
123
124 #[test]
125 fn test_plugin_name_empty() {
126 let identity = PluginIdentity {
127 magic: CC_LB_PLUGIN_MAGIC,
128 abi_envelope: 1,
129 plugin_name: String::new(),
130 plugin_version: "1.0.0".to_string(),
131 };
132 assert_eq!(identity.validate(), Err(IdentityError::PluginNameEmpty));
133 }
134
135 #[test]
136 fn test_plugin_name_starts_with_uppercase() {
137 let identity = PluginIdentity {
138 magic: CC_LB_PLUGIN_MAGIC,
139 abi_envelope: 1,
140 plugin_name: "MyPlugin".to_string(),
141 plugin_version: "1.0.0".to_string(),
142 };
143 assert_eq!(identity.validate(), Err(IdentityError::PluginNameInvalid));
144 }
145
146 #[test]
147 fn test_plugin_name_with_invalid_char() {
148 let identity = PluginIdentity {
149 magic: CC_LB_PLUGIN_MAGIC,
150 abi_envelope: 1,
151 plugin_name: "my.plugin".to_string(),
152 plugin_version: "1.0.0".to_string(),
153 };
154 assert_eq!(identity.validate(), Err(IdentityError::PluginNameInvalid));
155 }
156
157 #[test]
158 fn test_plugin_name_too_long() {
159 let identity = PluginIdentity {
160 magic: CC_LB_PLUGIN_MAGIC,
161 abi_envelope: 1,
162 plugin_name: "a".repeat(65),
163 plugin_version: "1.0.0".to_string(),
164 };
165 assert_eq!(identity.validate(), Err(IdentityError::PluginNameInvalid));
166 }
167
168 #[test]
169 fn test_plugin_name_max_length() {
170 let identity = PluginIdentity {
171 magic: CC_LB_PLUGIN_MAGIC,
172 abi_envelope: 1,
173 plugin_name: "a".repeat(64),
174 plugin_version: "1.0.0".to_string(),
175 };
176 assert!(identity.validate().is_ok());
177 }
178
179 #[test]
180 fn test_plugin_name_with_underscore_and_hyphen() {
181 let identity = PluginIdentity {
182 magic: CC_LB_PLUGIN_MAGIC,
183 abi_envelope: 1,
184 plugin_name: "my_plugin-v2".to_string(),
185 plugin_version: "1.0.0".to_string(),
186 };
187 assert!(identity.validate().is_ok());
188 }
189
190 #[test]
191 fn test_plugin_version_empty() {
192 let identity = PluginIdentity {
193 magic: CC_LB_PLUGIN_MAGIC,
194 abi_envelope: 1,
195 plugin_name: "my-plugin".to_string(),
196 plugin_version: String::new(),
197 };
198 assert_eq!(identity.validate(), Err(IdentityError::PluginVersionEmpty));
199 }
200
201 #[test]
202 fn test_plugin_version_too_long() {
203 let identity = PluginIdentity {
204 magic: CC_LB_PLUGIN_MAGIC,
205 abi_envelope: 1,
206 plugin_name: "my-plugin".to_string(),
207 plugin_version: "v".repeat(33),
208 };
209 assert_eq!(
210 identity.validate(),
211 Err(IdentityError::PluginVersionTooLong)
212 );
213 }
214
215 #[test]
216 fn test_plugin_version_max_length() {
217 let identity = PluginIdentity {
218 magic: CC_LB_PLUGIN_MAGIC,
219 abi_envelope: 1,
220 plugin_name: "my-plugin".to_string(),
221 plugin_version: "v".repeat(32),
222 };
223 assert!(identity.validate().is_ok());
224 }
225
226 #[test]
227 fn test_serde_serialize_deserialize() {
228 let identity = PluginIdentity {
229 magic: CC_LB_PLUGIN_MAGIC,
230 abi_envelope: 1,
231 plugin_name: "my-plugin".to_string(),
232 plugin_version: "1.0.0".to_string(),
233 };
234
235 let json = serde_json::to_string(&identity).unwrap();
236 let deserialized: PluginIdentity = serde_json::from_str(&json).unwrap();
237
238 assert_eq!(identity, deserialized);
239 }
240
241 #[test]
242 fn test_serde_deny_unknown_fields() {
243 let json = r#"{"magic":[204,27,112,16,0,1,0,0],"abi_envelope":1,"plugin_name":"my-plugin","plugin_version":"1.0.0","unknown_field":"value"}"#;
244 let result: Result<PluginIdentity, _> = serde_json::from_str(json);
245 assert!(result.is_err());
246 }
247}