coding_agent_search/pages/
archive_config.rs1use serde::{Deserialize, Serialize};
6
7use super::encrypt::EncryptionConfig;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum ArchiveConfig {
13 Encrypted(EncryptionConfig),
15 Unencrypted(UnencryptedConfig),
17}
18
19impl ArchiveConfig {
20 pub fn is_encrypted(&self) -> bool {
22 matches!(self, ArchiveConfig::Encrypted(_))
23 }
24
25 pub fn as_encrypted(&self) -> Option<&EncryptionConfig> {
27 match self {
28 ArchiveConfig::Encrypted(cfg) => Some(cfg),
29 ArchiveConfig::Unencrypted(_) => None,
30 }
31 }
32
33 pub fn as_unencrypted(&self) -> Option<&UnencryptedConfig> {
35 match self {
36 ArchiveConfig::Encrypted(_) => None,
37 ArchiveConfig::Unencrypted(cfg) => Some(cfg),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct UnencryptedConfig {
46 pub encrypted: bool,
48 pub version: String,
50 pub payload: UnencryptedPayload,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub warning: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct UnencryptedPayload {
61 pub path: String,
63 pub format: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub size_bytes: Option<u64>,
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn make_unencrypted_payload() -> UnencryptedPayload {
76 UnencryptedPayload {
77 path: "data.sqlite".to_string(),
78 format: "sqlite".to_string(),
79 size_bytes: None,
80 }
81 }
82
83 fn make_unencrypted_config() -> UnencryptedConfig {
85 UnencryptedConfig {
86 encrypted: false,
87 version: "1.0".to_string(),
88 payload: make_unencrypted_payload(),
89 warning: None,
90 }
91 }
92
93 fn make_encryption_config() -> EncryptionConfig {
95 use crate::pages::encrypt::{Argon2Params, PayloadMeta};
96
97 EncryptionConfig {
98 version: 1,
99 export_id: "AAAAAAAAAAAAAAAAAAAAAA==".to_string(),
100 base_nonce: "AAAAAAAAAAAAAAA=".to_string(),
101 compression: "deflate".to_string(),
102 kdf_defaults: Argon2Params::default(),
103 payload: PayloadMeta {
104 chunk_size: 8 * 1024 * 1024,
105 chunk_count: 1,
106 total_compressed_size: 1024,
107 total_plaintext_size: 2048,
108 files: vec!["chunk_0".to_string()],
109 },
110 key_slots: vec![],
111 }
112 }
113
114 #[test]
117 fn test_is_encrypted_returns_true_for_encrypted_variant() {
118 let config = ArchiveConfig::Encrypted(make_encryption_config());
119 assert!(config.is_encrypted());
120 }
121
122 #[test]
123 fn test_is_encrypted_returns_false_for_unencrypted_variant() {
124 let config = ArchiveConfig::Unencrypted(make_unencrypted_config());
125 assert!(!config.is_encrypted());
126 }
127
128 #[test]
131 fn test_as_encrypted_returns_some_for_encrypted_variant() {
132 let inner = make_encryption_config();
133 let config = ArchiveConfig::Encrypted(inner.clone());
134 let result = config.as_encrypted();
135 assert!(result.is_some());
136 assert_eq!(result.unwrap().version, inner.version);
137 assert_eq!(result.unwrap().export_id, inner.export_id);
138 }
139
140 #[test]
141 fn test_as_encrypted_returns_none_for_unencrypted_variant() {
142 let config = ArchiveConfig::Unencrypted(make_unencrypted_config());
143 assert!(config.as_encrypted().is_none());
144 }
145
146 #[test]
149 fn test_as_unencrypted_returns_some_for_unencrypted_variant() {
150 let inner = make_unencrypted_config();
151 let config = ArchiveConfig::Unencrypted(inner.clone());
152 let result = config.as_unencrypted();
153 assert!(result.is_some());
154 assert_eq!(result.unwrap().version, inner.version);
155 assert!(!result.unwrap().encrypted);
156 }
157
158 #[test]
159 fn test_as_unencrypted_returns_none_for_encrypted_variant() {
160 let config = ArchiveConfig::Encrypted(make_encryption_config());
161 assert!(config.as_unencrypted().is_none());
162 }
163
164 #[test]
167 fn test_unencrypted_config_serialization_roundtrip() {
168 let original = make_unencrypted_config();
169 let json = serde_json::to_string(&original).expect("serialize");
170 let deserialized: UnencryptedConfig = serde_json::from_str(&json).expect("deserialize");
171
172 assert_eq!(original.encrypted, deserialized.encrypted);
173 assert_eq!(original.version, deserialized.version);
174 assert_eq!(original.payload.path, deserialized.payload.path);
175 assert_eq!(original.payload.format, deserialized.payload.format);
176 assert_eq!(original.warning, deserialized.warning);
177 }
178
179 #[test]
180 fn test_unencrypted_config_with_optional_fields_roundtrip() {
181 let original = UnencryptedConfig {
182 encrypted: false,
183 version: "2.0".to_string(),
184 payload: UnencryptedPayload {
185 path: "archive/data.sqlite".to_string(),
186 format: "sqlite".to_string(),
187 size_bytes: Some(123456),
188 },
189 warning: Some("This bundle is unencrypted!".to_string()),
190 };
191
192 let json = serde_json::to_string(&original).expect("serialize");
193 let deserialized: UnencryptedConfig = serde_json::from_str(&json).expect("deserialize");
194
195 assert_eq!(original.payload.size_bytes, deserialized.payload.size_bytes);
196 assert_eq!(original.warning, deserialized.warning);
197 }
198
199 #[test]
200 fn test_archive_config_unencrypted_roundtrip() {
201 let original = ArchiveConfig::Unencrypted(make_unencrypted_config());
202 let json = serde_json::to_string(&original).expect("serialize");
203 let deserialized: ArchiveConfig = serde_json::from_str(&json).expect("deserialize");
204
205 assert!(!deserialized.is_encrypted());
206 let inner = deserialized
207 .as_unencrypted()
208 .expect("should be unencrypted");
209 assert_eq!(inner.version, "1.0");
210 }
211
212 #[test]
213 fn test_archive_config_encrypted_roundtrip() {
214 let original = ArchiveConfig::Encrypted(make_encryption_config());
215 let json = serde_json::to_string(&original).expect("serialize");
216 let deserialized: ArchiveConfig = serde_json::from_str(&json).expect("deserialize");
217
218 assert!(deserialized.is_encrypted());
219 let inner = deserialized.as_encrypted().expect("should be encrypted");
220 assert_eq!(inner.version, 1);
221 assert_eq!(inner.compression, "deflate");
222 }
223
224 #[test]
227 fn test_untagged_deserialize_encrypted_json() {
228 let json = r#"{
230 "version": 1,
231 "export_id": "dGVzdGV4cG9ydGlkMTIz",
232 "base_nonce": "dGVzdG5vbmNlMTI=",
233 "compression": "gzip",
234 "kdf_defaults": {
235 "memory_kb": 65536,
236 "iterations": 3,
237 "parallelism": 4
238 },
239 "payload": {
240 "chunk_size": 4194304,
241 "chunk_count": 2,
242 "total_compressed_size": 2048,
243 "total_plaintext_size": 4096,
244 "files": ["chunk_0", "chunk_1"]
245 },
246 "key_slots": []
247 }"#;
248
249 let config: ArchiveConfig = serde_json::from_str(json).expect("deserialize");
250 assert!(config.is_encrypted());
251 }
252
253 #[test]
254 fn test_untagged_deserialize_unencrypted_json() {
255 let json = r#"{
257 "encrypted": false,
258 "version": "1.0",
259 "payload": {
260 "path": "payload.sqlite",
261 "format": "sqlite"
262 }
263 }"#;
264
265 let config: ArchiveConfig = serde_json::from_str(json).expect("deserialize");
266 assert!(!config.is_encrypted());
267 let inner = config.as_unencrypted().expect("should be unencrypted");
268 assert_eq!(inner.payload.path, "payload.sqlite");
269 }
270
271 #[test]
272 fn test_untagged_deserialize_rejects_unknown_top_level_field() {
273 let json = r#"{
274 "encrypted": false,
275 "version": "1.0",
276 "payload": {
277 "path": "payload.sqlite",
278 "format": "sqlite"
279 },
280 "totally_unknown_field": 123
281 }"#;
282
283 serde_json::from_str::<ArchiveConfig>(json).expect_err("should reject unknown");
284 }
285
286 #[test]
287 fn test_untagged_deserialize_rejects_unknown_nested_payload_field() {
288 let json = r#"{
289 "encrypted": false,
290 "version": "1.0",
291 "payload": {
292 "path": "payload.sqlite",
293 "format": "sqlite",
294 "extra_payload_field": true
295 }
296 }"#;
297
298 serde_json::from_str::<ArchiveConfig>(json).expect_err("should reject unknown");
299 }
300
301 #[test]
304 fn test_unencrypted_payload_minimal() {
305 let payload = UnencryptedPayload {
306 path: "db.sqlite".to_string(),
307 format: "sqlite".to_string(),
308 size_bytes: None,
309 };
310
311 let json = serde_json::to_string(&payload).expect("serialize");
312 assert!(!json.contains("size_bytes"));
314
315 let deserialized: UnencryptedPayload = serde_json::from_str(&json).expect("deserialize");
316 assert_eq!(deserialized.path, "db.sqlite");
317 assert!(deserialized.size_bytes.is_none());
318 }
319
320 #[test]
321 fn test_unencrypted_payload_with_size() {
322 let payload = UnencryptedPayload {
323 path: "large.sqlite".to_string(),
324 format: "sqlite".to_string(),
325 size_bytes: Some(1_000_000),
326 };
327
328 let json = serde_json::to_string(&payload).expect("serialize");
329 assert!(json.contains("size_bytes"));
330 assert!(json.contains("1000000"));
331
332 let deserialized: UnencryptedPayload = serde_json::from_str(&json).expect("deserialize");
333 assert_eq!(deserialized.size_bytes, Some(1_000_000));
334 }
335
336 #[test]
339 fn test_unencrypted_config_warning_skipped_when_none() {
340 let config = make_unencrypted_config();
341 let json = serde_json::to_string(&config).expect("serialize");
342 assert!(!json.contains("warning"));
343 }
344
345 #[test]
346 fn test_unencrypted_config_warning_included_when_some() {
347 let mut config = make_unencrypted_config();
348 config.warning = Some("Be careful!".to_string());
349 let json = serde_json::to_string(&config).expect("serialize");
350 assert!(json.contains("warning"));
351 assert!(json.contains("Be careful!"));
352 }
353
354 #[test]
355 fn test_clone_preserves_all_fields() {
356 let original = UnencryptedConfig {
357 encrypted: false,
358 version: "3.0".to_string(),
359 payload: UnencryptedPayload {
360 path: "test.sqlite".to_string(),
361 format: "sqlite".to_string(),
362 size_bytes: Some(999),
363 },
364 warning: Some("Cloned warning".to_string()),
365 };
366
367 let cloned = original.clone();
368 assert_eq!(original.encrypted, cloned.encrypted);
369 assert_eq!(original.version, cloned.version);
370 assert_eq!(original.payload.path, cloned.payload.path);
371 assert_eq!(original.payload.size_bytes, cloned.payload.size_bytes);
372 assert_eq!(original.warning, cloned.warning);
373 }
374
375 #[test]
376 fn test_archive_config_clone() {
377 let original = ArchiveConfig::Unencrypted(make_unencrypted_config());
378 let cloned = original.clone();
379 assert!(!cloned.is_encrypted());
380 }
381
382 #[test]
383 fn test_debug_impl_exists() {
384 let config = make_unencrypted_config();
385 let debug_str = format!("{:?}", config);
386 assert!(debug_str.contains("UnencryptedConfig"));
387 assert!(debug_str.contains("version"));
388 }
389
390 #[test]
391 fn test_archive_config_debug_impl() {
392 let encrypted = ArchiveConfig::Encrypted(make_encryption_config());
393 let debug_str = format!("{:?}", encrypted);
394 assert!(debug_str.contains("Encrypted"));
395
396 let unencrypted = ArchiveConfig::Unencrypted(make_unencrypted_config());
397 let debug_str = format!("{:?}", unencrypted);
398 assert!(debug_str.contains("Unencrypted"));
399 }
400}