Skip to main content

lora_database/snapshot/
json.rs

1//! JSON option / credential adapters used by the language bindings.
2//!
3//! The HTTP, WASM, FFI, and node bindings all hand snapshot save and
4//! load options across the FFI boundary as `serde_json::Value`. This
5//! module owns the validation and shape normalization so the rest of
6//! the database can take typed [`SnapshotOptions`] / [`SnapshotCredentials`]
7//! values directly.
8//!
9//! Two public entry points sit at the top:
10//!
11//! * [`snapshot_options_from_json`] — build save options.
12//! * [`snapshot_credentials_from_json`] — build load credentials.
13//!
14//! All other helpers (compression / encryption / KDF / field-extraction)
15//! are private — bindings consume only the two public entry points.
16
17use anyhow::{anyhow, Result};
18
19use lora_snapshot::{
20    Compression, EncryptionKey, PasswordKdfParams, SnapshotCredentials, SnapshotEncryption,
21    SnapshotOptions, SnapshotPassword,
22};
23
24const DEFAULT_SNAPSHOT_KEY_ID: &str = "default";
25
26/// Build snapshot save options from the JSON shape used by the language
27/// bindings.
28///
29/// Supported shape:
30///
31/// `{ compression?: "none" | "gzip" | { format: "gzip", level?: number },
32///    encryption?: { type: "password", keyId?: string, password: string,
33///                   params?: { memoryCostKib?: number, timeCost?: number,
34///                              parallelism?: number } } }`
35///
36/// `encryption` may also be a raw 32-byte key object with
37/// `{ type: "key", keyId?: string, key: number[] }`.
38pub fn snapshot_options_from_json(value: Option<serde_json::Value>) -> Result<SnapshotOptions> {
39    let Some(value) = value else {
40        return Ok(SnapshotOptions {
41            compression: Compression::None,
42            encryption: None,
43        });
44    };
45    if value.is_null() {
46        return Ok(SnapshotOptions {
47            compression: Compression::None,
48            encryption: None,
49        });
50    }
51
52    let compression = match value.get("compression") {
53        Some(value) => parse_snapshot_compression_json(value)?,
54        None => Compression::None,
55    };
56    let encryption = parse_snapshot_credentials_json(Some(value))?;
57
58    Ok(SnapshotOptions {
59        compression,
60        encryption,
61    })
62}
63
64/// Build snapshot load credentials from the JSON shape used by the language
65/// bindings. The credential object may be supplied directly, or under
66/// `credentials` / `encryption` so the same options object can be reused for
67/// save and load.
68pub fn snapshot_credentials_from_json(
69    value: Option<serde_json::Value>,
70) -> Result<Option<SnapshotCredentials>> {
71    let Some(value) = value else {
72        return Ok(None);
73    };
74    if value.is_null() {
75        return Ok(None);
76    }
77    parse_snapshot_credentials_json(Some(value))
78}
79
80fn parse_snapshot_compression_json(value: &serde_json::Value) -> Result<Compression> {
81    match value {
82        serde_json::Value::Null => Ok(Compression::None),
83        serde_json::Value::String(format) => snapshot_compression_from_parts(format, None),
84        serde_json::Value::Object(obj) => {
85            let format =
86                string_field(obj, &["format", "type"])?.unwrap_or_else(|| "none".to_string());
87            let level = u32_field(obj, &["level"])?;
88            snapshot_compression_from_parts(&format, level)
89        }
90        _ => Err(anyhow!(
91            "snapshot compression must be a string or object with a format field"
92        )),
93    }
94}
95
96fn snapshot_compression_from_parts(format: &str, level: Option<u32>) -> Result<Compression> {
97    match format {
98        "none" | "identity" | "uncompressed" => Ok(Compression::None),
99        "gzip" => {
100            let level = level.unwrap_or(1);
101            if level > 9 {
102                return Err(anyhow!(
103                    "gzip snapshot compression level must be between 0 and 9"
104                ));
105            }
106            Ok(Compression::Gzip { level })
107        }
108        other => Err(anyhow!("unknown snapshot compression '{other}'")),
109    }
110}
111
112fn parse_snapshot_credentials_json(
113    value: Option<serde_json::Value>,
114) -> Result<Option<SnapshotCredentials>> {
115    let Some(value) = value else {
116        return Ok(None);
117    };
118    if value.is_null() {
119        return Ok(None);
120    }
121
122    let credential_value = if let Some(credentials) = value.get("credentials") {
123        credentials
124    } else if let Some(encryption) = value.get("encryption") {
125        encryption
126    } else if looks_like_snapshot_encryption(&value) {
127        &value
128    } else {
129        return Ok(None);
130    };
131
132    if credential_value.is_null() {
133        return Ok(None);
134    }
135
136    Ok(Some(parse_snapshot_encryption_json(credential_value)?))
137}
138
139fn looks_like_snapshot_encryption(value: &serde_json::Value) -> bool {
140    let Some(obj) = value.as_object() else {
141        return false;
142    };
143    obj.contains_key("password")
144        || obj.contains_key("key")
145        || obj.contains_key("keyBytes")
146        || obj.contains_key("key_bytes")
147}
148
149fn parse_snapshot_encryption_json(value: &serde_json::Value) -> Result<SnapshotEncryption> {
150    let serde_json::Value::Object(obj) = value else {
151        return Err(anyhow!("snapshot encryption must be an object"));
152    };
153
154    let kind = string_field(obj, &["type", "kind"])?.unwrap_or_else(|| {
155        if obj.contains_key("key") || obj.contains_key("keyBytes") || obj.contains_key("key_bytes")
156        {
157            "key".to_string()
158        } else {
159            "password".to_string()
160        }
161    });
162
163    match kind.as_str() {
164        "password" | "passphrase" => {
165            let key_id = string_field(obj, &["keyId", "key_id"])?
166                .unwrap_or_else(|| DEFAULT_SNAPSHOT_KEY_ID.to_string());
167            let password = required_string_field(obj, &["password"])?;
168            let params = parse_password_kdf_params(
169                obj.get("params")
170                    .or_else(|| obj.get("kdfParams"))
171                    .or_else(|| obj.get("kdf_params")),
172            )?;
173            Ok(SnapshotEncryption::Password(SnapshotPassword::with_params(
174                key_id, password, params,
175            )))
176        }
177        "key" | "raw_key" | "rawKey" => {
178            let key_id = string_field(obj, &["keyId", "key_id"])?
179                .unwrap_or_else(|| DEFAULT_SNAPSHOT_KEY_ID.to_string());
180            let key = required_key_field(obj, &["key", "keyBytes", "key_bytes"])?;
181            Ok(SnapshotEncryption::Key(EncryptionKey::new(key_id, key)))
182        }
183        other => Err(anyhow!("unknown snapshot encryption type '{other}'")),
184    }
185}
186
187fn parse_password_kdf_params(value: Option<&serde_json::Value>) -> Result<PasswordKdfParams> {
188    let Some(value) = value else {
189        return Ok(PasswordKdfParams::interactive());
190    };
191    if value.is_null() {
192        return Ok(PasswordKdfParams::interactive());
193    }
194    let serde_json::Value::Object(obj) = value else {
195        return Err(anyhow!("snapshot password params must be an object"));
196    };
197
198    let defaults = PasswordKdfParams::interactive();
199    let memory_cost_kib =
200        u32_field(obj, &["memoryCostKib", "memory_cost_kib"])?.unwrap_or(defaults.memory_cost_kib);
201    let time_cost = u32_field(obj, &["timeCost", "time_cost"])?.unwrap_or(defaults.time_cost);
202    let parallelism = u32_field(obj, &["parallelism"])?.unwrap_or(defaults.parallelism);
203
204    if memory_cost_kib == 0 || time_cost == 0 || parallelism == 0 {
205        return Err(anyhow!(
206            "snapshot password params memoryCostKib, timeCost, and parallelism must be greater than zero"
207        ));
208    }
209
210    Ok(PasswordKdfParams {
211        memory_cost_kib,
212        time_cost,
213        parallelism,
214    })
215}
216
217fn string_field(
218    obj: &serde_json::Map<String, serde_json::Value>,
219    names: &[&str],
220) -> Result<Option<String>> {
221    for name in names {
222        if let Some(value) = obj.get(*name) {
223            return match value {
224                serde_json::Value::Null => Ok(None),
225                serde_json::Value::String(value) => Ok(Some(value.clone())),
226                _ => Err(anyhow!("snapshot field '{name}' must be a string")),
227            };
228        }
229    }
230    Ok(None)
231}
232
233fn required_string_field(
234    obj: &serde_json::Map<String, serde_json::Value>,
235    names: &[&str],
236) -> Result<String> {
237    string_field(obj, names)?.ok_or_else(|| anyhow!("snapshot field '{}' is required", names[0]))
238}
239
240fn u32_field(
241    obj: &serde_json::Map<String, serde_json::Value>,
242    names: &[&str],
243) -> Result<Option<u32>> {
244    for name in names {
245        if let Some(value) = obj.get(*name) {
246            return match value {
247                serde_json::Value::Null => Ok(None),
248                serde_json::Value::Number(number) => {
249                    let Some(value) = number.as_u64() else {
250                        return Err(anyhow!(
251                            "snapshot field '{name}' must be a non-negative integer"
252                        ));
253                    };
254                    if value > u32::MAX as u64 {
255                        return Err(anyhow!("snapshot field '{name}' is too large"));
256                    }
257                    Ok(Some(value as u32))
258                }
259                _ => Err(anyhow!("snapshot field '{name}' must be an integer")),
260            };
261        }
262    }
263    Ok(None)
264}
265
266fn required_key_field(
267    obj: &serde_json::Map<String, serde_json::Value>,
268    names: &[&str],
269) -> Result<[u8; 32]> {
270    for name in names {
271        if let Some(value) = obj.get(*name) {
272            return parse_key_bytes(value, name);
273        }
274    }
275    Err(anyhow!("snapshot field '{}' is required", names[0]))
276}
277
278fn parse_key_bytes(value: &serde_json::Value, field_name: &str) -> Result<[u8; 32]> {
279    let serde_json::Value::Array(values) = value else {
280        return Err(anyhow!(
281            "snapshot field '{field_name}' must be an array of 32 byte values"
282        ));
283    };
284    if values.len() != 32 {
285        return Err(anyhow!(
286            "snapshot field '{field_name}' must contain exactly 32 byte values"
287        ));
288    }
289
290    let mut out = [0u8; 32];
291    for (idx, value) in values.iter().enumerate() {
292        let Some(byte) = value.as_u64() else {
293            return Err(anyhow!(
294                "snapshot field '{field_name}' item {idx} must be a byte integer"
295            ));
296        };
297        if byte > u8::MAX as u64 {
298            return Err(anyhow!(
299                "snapshot field '{field_name}' item {idx} must be between 0 and 255"
300            ));
301        }
302        out[idx] = byte as u8;
303    }
304    Ok(out)
305}