Skip to main content

serialization_macros/
serialization_macros.rs

1//! Demonstrates the `#[serializable]`, `#[serialize]`, and `#[deserialize]`
2//! attribute macros from the `serialization` feature.
3//!
4//! All three macros are built on top of the same ChaCha20-Poly1305 AEAD
5//! primitives exposed by [`toolkit_zero::serialization`].
6//!
7//! | Macro             | Purpose                                              |
8//! |-------------------|------------------------------------------------------|
9//! | `#[serializable]` | Derives `seal` / `open` methods on a struct          |
10//! | `#[serialize]`    | Encrypts a value into a variable or a file           |
11//! | `#[deserialize]`  | Decrypts a variable or a file into a typed value     |
12//!
13//! ## `#[serializable]`
14//!
15//! Attaching `#[serializable]` to a struct generates:
16//!
17//! - `instance.seal(key: Option<String>) -> Result<Vec<u8>, Error>` — encrypts
18//! - `Type::open(bytes: &[u8], key: Option<String>) -> Result<Type, Error>` — decrypts
19//!
20//! Per-field keys are also supported via `#[serializable(key = "...")]` on a
21//! field, which generates `instance.seal_<field>()` / `Type::open_<field>()`.
22//!
23//! ## `#[serialize]` / `#[deserialize]`
24//!
25//! These are statement-level macros that expand _inside_ a function body.
26//!
27//! **Variable mode** (`#[serialize(expr)]` / `#[deserialize(blob_var)]`):
28//! ```text
29//! #[serialize(cfg)]
30//! fn blob() -> Vec<u8> {}
31//! // ↑ expands to: let blob: Vec<u8> = toolkit_zero::serialization::seal(&cfg, None)?;
32//!
33//! #[deserialize(blob)]
34//! fn cfg_back() -> Config {}
35//! // ↑ expands to: let cfg_back: Config = toolkit_zero::serialization::open::<Config, _>(&blob, None)?;
36//! ```
37//!
38//! **File mode** (add `path = "..."` to both):
39//! ```text
40//! #[serialize(cfg, path = "/tmp/config.bin")]
41//! fn _write() {}
42//! // ↑ writes the encrypted bytes to the file; no binding is produced.
43//!
44//! #[deserialize(path = "/tmp/config.bin")]
45//! fn cfg_from_file() -> Config {}
46//! // ↑ reads the file and decrypts it; binds the result to `cfg_from_file`.
47//! ```
48//!
49//! An optional `key = "<expression>"` argument selects a custom encryption key on
50//! both macros.
51//!
52//! Run with:
53//! ```sh
54//! cargo run --example serialization_macros --features serialization
55//! ```
56
57use toolkit_zero::serialization::{serializable, serialize, deserialize};
58
59// ─── Shared data types ────────────────────────────────────────────────────────
60
61/// A nested config struct exercised by `#[serializable]`.
62#[serializable]
63#[derive(Debug, PartialEq, Clone)]
64struct AppConfig {
65    debug: bool,
66    max_conn: u32,
67    hostname: String,
68}
69
70/// Per-field key annotation: `password` is sealed with a dedicated key.
71#[serializable]
72#[derive(Debug, PartialEq, Clone)]
73struct Credentials {
74    pub username: String,
75    /// Sealed independently with the baked-in key `"per-field-secret"`.
76    #[serializable(key = "per-field-secret")]
77    pub password: String,
78}
79
80/// Struct used with `#[serialize]` / `#[deserialize]` variable mode.
81#[serializable]
82#[derive(Debug, PartialEq, Clone)]
83struct Payload {
84    id:   u64,
85    data: String,
86}
87
88// ─── Main ─────────────────────────────────────────────────────────────────────
89
90fn main() -> Result<(), Box<dyn std::error::Error>> {
91    println!("=== serialization macros demo ===\n");
92    demo_serializable()?;
93    demo_serializable_field_key()?;
94    demo_serialize_variable_mode()?;
95    demo_serialize_file_mode()?;
96    println!("\nAll demos completed successfully ✓");
97    Ok(())
98}
99
100// ─── Demo: #[serializable] — struct-level seal / open ─────────────────────────
101
102fn demo_serializable() -> Result<(), Box<dyn std::error::Error>> {
103    println!("── #[serializable] struct-level round-trips ──────────────────────");
104
105    let cfg = AppConfig {
106        debug:    true,
107        max_conn: 64,
108        hostname: "localhost".into(),
109    };
110
111    // ── Default key (None) ────────────────────────────────────────────────────
112    // `seal` encrypts with the library's built-in default key.
113    // `open` must use the same key to succeed.
114    let blob = cfg.seal(None)?;
115    let recovered = AppConfig::open(&blob, None)?;
116    assert_eq!(cfg, recovered);
117    println!("  default key  : seal → {} bytes, open → {:?}", blob.len(), recovered);
118
119    // ── Custom key ────────────────────────────────────────────────────────────
120    let blob2 = cfg.seal(Some("my-secret-key".into()))?;
121    let recovered2 = AppConfig::open(&blob2, Some("my-secret-key".into()))?;
122    assert_eq!(cfg, recovered2);
123    println!("  custom key   : seal → {} bytes, open → {:?}", blob2.len(), recovered2);
124
125    // ── Wrong key must fail ───────────────────────────────────────────────────
126    let bad = AppConfig::open(&blob2, Some("wrong-key".into()));
127    assert!(bad.is_err(), "opening with the wrong key must fail");
128    println!("  wrong key    : open → Err (expected) ✓");
129
130    println!();
131    Ok(())
132}
133
134// ─── Demo: #[serializable] — per-field key annotation ────────────────────────
135
136fn demo_serializable_field_key() -> Result<(), Box<dyn std::error::Error>> {
137    println!("── #[serializable(key = \"...\")]  per-field helpers ───────────────");
138
139    let creds = Credentials {
140        username: "alice".into(),
141        password: "hunter2".into(),
142    };
143
144    // Per-field helpers use the key baked into the annotation.
145    // `seal_password()` uses `"per-field-secret"` without the caller supplying it.
146    let pw_bytes  = creds.seal_password()?;
147    let pw_back   = Credentials::open_password(&pw_bytes)?;
148    assert_eq!("hunter2", pw_back);
149    println!("  seal_password  → {} bytes", pw_bytes.len());
150    println!("  open_password  → {:?}", pw_back);
151
152    // Full-struct helpers still exist alongside the per-field ones.
153    let full_blob = creds.seal(None)?;
154    let full_back = Credentials::open(&full_blob, None)?;
155    assert_eq!(creds, full_back);
156    println!("  full seal/open → {:?}", full_back);
157
158    println!();
159    Ok(())
160}
161
162// ─── Demo: #[serialize] / #[deserialize] — variable mode ─────────────────────
163
164fn demo_serialize_variable_mode() -> Result<(), Box<dyn std::error::Error>> {
165    println!("── #[serialize] / #[deserialize]  variable mode ─────────────────");
166
167    let payload = Payload { id: 42, data: "hello, world".into() };
168
169    // ── Default key ───────────────────────────────────────────────────────────
170    // `#[serialize(payload)]` expands to:
171    //     let blob: Vec<u8> = toolkit_zero::serialization::seal(&payload, None)?;
172    #[serialize(payload)]
173    fn blob() -> Vec<u8> {}
174
175    // `#[deserialize(blob)]` expands to:
176    //     let restored: Payload = toolkit_zero::serialization::open::<Payload, _>(&blob, None)?;
177    #[deserialize(blob)]
178    fn restored() -> Payload {}
179
180    assert_eq!(payload, restored);
181    println!("  default key  : {} bytes → {:?}", blob.len(), restored);
182
183    // ── Custom key ────────────────────────────────────────────────────────────
184    // The `key = <expr>` argument accepts any expression that evaluates to `String`.
185    #[serialize(payload, key = "custom-key".to_string())]
186    fn blob_keyed() -> Vec<u8> {}
187
188    #[deserialize(blob_keyed, key = "custom-key".to_string())]
189    fn restored_keyed() -> Payload {}
190
191    assert_eq!(payload, restored_keyed);
192    println!("  custom key   : {} bytes → {:?}", blob_keyed.len(), restored_keyed);
193
194    // ── Cross-key failure check ───────────────────────────────────────────────
195    let wrong = toolkit_zero::serialization::open::<Payload, String>(&blob_keyed, None);
196    assert!(wrong.is_err(), "decrypting with the wrong key must fail");
197    println!("  wrong key    : open → Err (expected) ✓");
198
199    println!();
200    Ok(())
201}
202
203// ─── Demo: #[serialize] / #[deserialize] — file mode ─────────────────────────
204
205fn demo_serialize_file_mode() -> Result<(), Box<dyn std::error::Error>> {
206    println!("── #[serialize] / #[deserialize]  file mode ─────────────────────");
207
208    let payload = Payload { id: 99, data: "persisted value".into() };
209
210    // ── Default key, written to /tmp ──────────────────────────────────────────
211    // `path = "..."` writes the encrypted bytes to the given file.
212    // No variable binding is produced; the function name is ignored.
213    #[serialize(payload, path = "/tmp/toolkit_zero_demo.bin")]
214    fn _write() {}
215
216    // `#[deserialize(path = "...")]` reads the file and decrypts it.
217    #[deserialize(path = "/tmp/toolkit_zero_demo.bin")]
218    fn loaded() -> Payload {}
219
220    assert_eq!(payload, loaded);
221    println!("  default key  : wrote /tmp/toolkit_zero_demo.bin → {:?}", loaded);
222
223    // ── Custom key, different file ─────────────────────────────────────────────
224    #[serialize(payload, path = "/tmp/toolkit_zero_demo_keyed.bin", key = "file-key".to_string())]
225    fn _write_keyed() {}
226
227    #[deserialize(path = "/tmp/toolkit_zero_demo_keyed.bin", key = "file-key".to_string())]
228    fn loaded_keyed() -> Payload {}
229
230    assert_eq!(payload, loaded_keyed);
231    println!("  custom key   : wrote /tmp/toolkit_zero_demo_keyed.bin → {:?}", loaded_keyed);
232
233    // Clean up temp files.
234    std::fs::remove_file("/tmp/toolkit_zero_demo.bin").ok();
235    std::fs::remove_file("/tmp/toolkit_zero_demo_keyed.bin").ok();
236
237    println!();
238    Ok(())
239}