Skip to main content

coding_agent_search/pages/
archive_config.rs

1//! Archive configuration types for pages bundles.
2//!
3//! Supports both encrypted and unencrypted bundles via an untagged enum.
4
5use serde::{Deserialize, Serialize};
6
7use super::encrypt::EncryptionConfig;
8
9/// Supported archive configuration formats.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum ArchiveConfig {
13    /// Encrypted bundle configuration (default).
14    Encrypted(EncryptionConfig),
15    /// Unencrypted bundle configuration.
16    Unencrypted(UnencryptedConfig),
17}
18
19impl ArchiveConfig {
20    /// Returns true if this config represents an encrypted bundle.
21    pub fn is_encrypted(&self) -> bool {
22        matches!(self, ArchiveConfig::Encrypted(_))
23    }
24
25    /// Get the encrypted config if available.
26    pub fn as_encrypted(&self) -> Option<&EncryptionConfig> {
27        match self {
28            ArchiveConfig::Encrypted(cfg) => Some(cfg),
29            ArchiveConfig::Unencrypted(_) => None,
30        }
31    }
32
33    /// Get the unencrypted config if available.
34    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/// Unencrypted bundle configuration.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct UnencryptedConfig {
46    /// Whether the bundle is encrypted (must be false).
47    pub encrypted: bool,
48    /// Config version.
49    pub version: String,
50    /// Payload descriptor.
51    pub payload: UnencryptedPayload,
52    /// Optional warning message for viewers.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub warning: Option<String>,
55}
56
57/// Unencrypted payload descriptor.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct UnencryptedPayload {
61    /// Relative path to the SQLite database payload.
62    pub path: String,
63    /// Payload format (e.g., "sqlite").
64    pub format: String,
65    /// Optional byte size of the payload.
66    #[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    // Helper to create a minimal UnencryptedPayload
75    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    // Helper to create a minimal UnencryptedConfig
84    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    // Helper to create a minimal EncryptionConfig for testing
94    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    // ==================== ArchiveConfig::is_encrypted() tests ====================
115
116    #[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    // ==================== ArchiveConfig::as_encrypted() tests ====================
129
130    #[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    // ==================== ArchiveConfig::as_unencrypted() tests ====================
147
148    #[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    // ==================== Serialization round-trip tests ====================
165
166    #[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    // ==================== Serde untagged behavior tests ====================
225
226    #[test]
227    fn test_untagged_deserialize_encrypted_json() {
228        // JSON that matches EncryptionConfig structure
229        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        // JSON that matches UnencryptedConfig structure
256        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    // ==================== UnencryptedPayload tests ====================
302
303    #[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        // size_bytes should be skipped when None
313        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    // ==================== Edge case tests ====================
337
338    #[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}