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}