1use std::sync::Arc;
19
20use crate::control::ControlStore;
21use crate::doc::{impl_doc, DocRef, ShardedMap};
22use crate::error::{Error, Result};
23
24pub const SETTINGS_REF: &str = "refs/gunnar/settings";
26pub const MAX_NAME_BYTES: usize = 128;
28pub const MAX_VALUE_BYTES: usize = 4096;
30pub const MAX_SETTINGS: usize = 256;
32
33#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
34#[serde(deny_unknown_fields)]
35struct SettingsDoc {
36 #[serde(default)]
38 values: ShardedMap<String>,
39}
40
41impl_doc!(SettingsDoc { values });
42
43#[derive(Clone, Debug)]
46pub struct SettingsStore {
47 doc: DocRef<SettingsDoc>,
48}
49
50pub fn check_name(name: &str) -> Result<()> {
52 let refuse = |reason: String| Err(Error::InvalidInput { what: "setting name", reason });
53 if name.is_empty() {
54 return refuse("empty".to_owned());
55 }
56 if name.len() > MAX_NAME_BYTES {
57 return refuse(format!("{} bytes, more than the {MAX_NAME_BYTES} a name may have", name.len()));
58 }
59 if !name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')) {
60 return refuse(format!("{name:?} has a character outside [a-z0-9._-]"));
61 }
62 if !name.as_bytes()[0].is_ascii_alphanumeric() {
63 return refuse(format!("{name:?} does not start with a letter or a digit"));
64 }
65 Ok(())
66}
67
68fn hex(b: &[u8]) -> String {
69 b.iter().map(|x| format!("{x:02x}")).collect()
70}
71
72fn unhex(s: &str) -> Result<Vec<u8>> {
73 let bad = || Error::InvalidInput { what: "stored setting", reason: "not hex: the settings document is damaged".to_owned() };
74 if s.len() % 2 != 0 {
75 return Err(bad());
76 }
77 (0..s.len()).step_by(2).map(|i| s.get(i..i + 2).and_then(|p| u8::from_str_radix(p, 16).ok()).ok_or_else(bad)).collect()
78}
79
80impl SettingsStore {
81 pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
83 Ok(SettingsStore { doc: DocRef::new(store, SETTINGS_REF)? })
84 }
85
86 pub fn get(&self, name: &str) -> Result<Option<Vec<u8>>> {
88 check_name(name)?;
89 self.doc.load()?.values.get(name)?.map(|v| unhex(&v)).transpose()
90 }
91
92 pub fn set(&self, name: &str, value: &[u8]) -> Result<Option<Vec<u8>>> {
95 check_name(name)?;
96 if value.len() > MAX_VALUE_BYTES {
97 return Err(Error::InvalidInput { what: "setting value", reason: format!("{} bytes, more than the {MAX_VALUE_BYTES} a value may have", value.len()) });
98 }
99 let previous = self.doc.update(|doc| {
100 if !doc.values.contains_key(name)? && doc.values.len() >= MAX_SETTINGS {
101 return Err(Error::InvalidInput { what: "setting", reason: format!("this server holds {MAX_SETTINGS} settings already, the most it keeps") });
102 }
103 doc.values.insert(name.to_owned(), hex(value))
104 })?;
105 previous.map(|v| unhex(&v)).transpose()
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn store() -> SettingsStore {
114 SettingsStore::new(Arc::new(crate::control::RedbControlStore::in_memory().expect("a store"))).expect("the document")
115 }
116
117 #[test]
118 fn a_setting_is_stored_replaced_and_read_back_and_an_unset_one_is_none() {
119 let s = store();
120 assert_eq!(s.get("console.a-switch").unwrap(), None);
121 assert_eq!(s.set("console.a-switch", b"on").unwrap(), None, "a new name replaced nothing");
122 assert_eq!(s.get("console.a-switch").unwrap().as_deref(), Some(&b"on"[..]));
123 assert_eq!(s.set("console.a-switch", b"off").unwrap().as_deref(), Some(&b"on"[..]), "the value it replaced");
124 assert_eq!(s.get("console.a-switch").unwrap().as_deref(), Some(&b"off"[..]));
125 assert_eq!(s.set("x", &[0, 255, 7]).unwrap(), None);
126 assert_eq!(s.get("x").unwrap().as_deref(), Some(&[0u8, 255, 7][..]), "any bytes");
127 }
128
129 #[test]
130 fn every_bound_is_refused_by_name() {
131 let s = store();
132 let words = |e: Error| e.to_string();
133 assert!(words(s.set("", b"").unwrap_err()).contains("empty"));
134 assert!(words(s.set(&"a".repeat(MAX_NAME_BYTES + 1), b"").unwrap_err()).contains("128"));
135 assert!(s.set(&"a".repeat(MAX_NAME_BYTES), b"").is_ok(), "at the bound it is a name");
136 assert!(words(s.set("Console.X", b"").unwrap_err()).contains("[a-z0-9._-]"));
137 assert!(words(s.set("a/b", b"").unwrap_err()).contains("[a-z0-9._-]"));
138 assert!(words(s.set(".hidden", b"").unwrap_err()).contains("start with"));
139 assert!(words(s.set("v", &vec![0; MAX_VALUE_BYTES + 1]).unwrap_err()).contains("4096"));
140 assert!(s.set("v", &vec![0; MAX_VALUE_BYTES]).is_ok(), "at the bound it is a value");
141 assert!(words(s.get("A").unwrap_err()).contains("[a-z0-9._-]"), "a read names the rule too");
142 }
143
144 #[test]
145 fn a_full_server_refuses_a_new_name_but_replaces_a_held_one() {
146 let s = store();
147 for i in 0..MAX_SETTINGS {
148 s.set(&format!("n{i}"), b"1").unwrap();
149 }
150 assert!(s.set("one-more", b"1").unwrap_err().to_string().contains(&MAX_SETTINGS.to_string()));
151 assert!(s.set("n0", b"2").is_ok(), "a held name is still replaced");
152 }
153}