aede_core/
user_preferences.rs

1//! Bounded client preferences, private to an immutable account owner.
2use super::UserData;
3use crate::{json::Json, store::StoreError};
4use std::collections::BTreeSet;
5
6/// One application-owned JSON document; no credentials belong in this table.
7#[derive(Debug, Clone)]
8pub struct ClientPreference {
9    /// Immutable personal owner.
10    pub owner: String,
11    /// ASCII application preference key.
12    pub key: String,
13    /// Valid bounded JSON, retained without rewriting unknown application fields.
14    pub value: String,
15}
16/// Validate storage bounds independently of any HTTP/frontend schema.
17pub fn valid_preference(key: &str, value: &str) -> bool {
18    !key.is_empty()
19        && key.len() <= 128
20        && key
21            .bytes()
22            .all(|b| b.is_ascii_alphanumeric() || b".-_:".contains(&b))
23        && value.len() <= 8192
24        && crate::json::parse(value).is_ok()
25}
26pub(super) fn validate(rows: &[ClientPreference]) -> Result<(), StoreError> {
27    if rows.len() > 8192 {
28        return Err(StoreError::Invalid("too many client preferences"));
29    }
30    let mut known = BTreeSet::new();
31    for row in rows {
32        if row.owner.is_empty()
33            || row.owner.len() > 256
34            || !valid_preference(&row.key, &row.value)
35            || !known.insert((&row.owner, &row.key))
36        {
37            return Err(StoreError::Invalid(
38                "invalid or duplicate client preference",
39            ));
40        }
41    }
42    Ok(())
43}
44pub(super) fn write(data: &UserData, root: &mut Json) {
45    if data.client_preferences.is_empty() {
46        return;
47    }
48    root.set(
49        "client_preferences",
50        Json::Arr(
51            data.client_preferences
52                .iter()
53                .map(|entry| {
54                    let mut row = Json::obj();
55                    row.set("owner", entry.owner.as_str().into());
56                    row.set("key", entry.key.as_str().into());
57                    row.set("value", entry.value.as_str().into());
58                    row
59                })
60                .collect(),
61        ),
62    );
63}
64pub(super) fn read(root: &Json) -> Result<Vec<ClientPreference>, StoreError> {
65    let Some(table) = root.get("client_preferences") else {
66        return Ok(vec![]);
67    };
68    let rows = table
69        .as_arr()
70        .ok_or(StoreError::Invalid("client preferences must be an array"))?;
71    if rows.len() > 8192 {
72        return Err(StoreError::Invalid("too many client preferences"));
73    }
74    let rows = rows
75        .iter()
76        .map(|row| {
77            let owner = row
78                .field_str("owner")
79                .ok_or(StoreError::Invalid("missing preference owner"))?;
80            let key = row
81                .field_str("key")
82                .ok_or(StoreError::Invalid("missing preference key"))?;
83            let value = row
84                .field_str("value")
85                .ok_or(StoreError::Invalid("missing preference value"))?;
86            Ok(ClientPreference { owner, key, value })
87        })
88        .collect::<Result<Vec<_>, StoreError>>()?;
89    validate(&rows)?;
90    Ok(rows)
91}
92#[cfg(test)]
93#[path = "user_preferences_tests.rs"]
94mod tests;