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| {
53 Err(Error::InvalidInput {
54 what: "setting name",
55 reason,
56 })
57 };
58 if name.is_empty() {
59 return refuse("empty".to_owned());
60 }
61 if name.len() > MAX_NAME_BYTES {
62 return refuse(format!(
63 "{} bytes, more than the {MAX_NAME_BYTES} a name may have",
64 name.len()
65 ));
66 }
67 if !name
68 .bytes()
69 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-'))
70 {
71 return refuse(format!("{name:?} has a character outside [a-z0-9._-]"));
72 }
73 if !name.as_bytes()[0].is_ascii_alphanumeric() {
74 return refuse(format!("{name:?} does not start with a letter or a digit"));
75 }
76 Ok(())
77}
78
79fn hex(b: &[u8]) -> String {
80 b.iter().map(|x| format!("{x:02x}")).collect()
81}
82
83fn unhex(s: &str) -> Result<Vec<u8>> {
84 let bad = || Error::InvalidInput {
85 what: "stored setting",
86 reason: "not hex: the settings document is damaged".to_owned(),
87 };
88 if s.len() % 2 != 0 {
89 return Err(bad());
90 }
91 (0..s.len())
92 .step_by(2)
93 .map(|i| {
94 s.get(i..i + 2)
95 .and_then(|p| u8::from_str_radix(p, 16).ok())
96 .ok_or_else(bad)
97 })
98 .collect()
99}
100
101impl SettingsStore {
102 pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
104 Ok(SettingsStore {
105 doc: DocRef::new(store, SETTINGS_REF)?,
106 })
107 }
108
109 pub fn get(&self, name: &str) -> Result<Option<Vec<u8>>> {
111 check_name(name)?;
112 self.doc
113 .load()?
114 .values
115 .get(name)?
116 .map(|v| unhex(&v))
117 .transpose()
118 }
119
120 pub fn set(&self, name: &str, value: &[u8]) -> Result<Option<Vec<u8>>> {
123 check_name(name)?;
124 if value.len() > MAX_VALUE_BYTES {
125 return Err(Error::InvalidInput {
126 what: "setting value",
127 reason: format!(
128 "{} bytes, more than the {MAX_VALUE_BYTES} a value may have",
129 value.len()
130 ),
131 });
132 }
133 let previous = self.doc.update(|doc| {
134 if !doc.values.contains_key(name)? && doc.values.len() >= MAX_SETTINGS {
135 return Err(Error::InvalidInput {
136 what: "setting",
137 reason: format!(
138 "this server holds {MAX_SETTINGS} settings already, the most it keeps"
139 ),
140 });
141 }
142 doc.values.insert(name.to_owned(), hex(value))
143 })?;
144 previous.map(|v| unhex(&v)).transpose()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn store() -> SettingsStore {
153 SettingsStore::new(Arc::new(
154 crate::control::RedbControlStore::in_memory().expect("a store"),
155 ))
156 .expect("the document")
157 }
158
159 #[test]
160 fn a_setting_is_stored_replaced_and_read_back_and_an_unset_one_is_none() {
161 let s = store();
162 assert_eq!(s.get("console.a-switch").unwrap(), None);
163 assert_eq!(
164 s.set("console.a-switch", b"on").unwrap(),
165 None,
166 "a new name replaced nothing"
167 );
168 assert_eq!(
169 s.get("console.a-switch").unwrap().as_deref(),
170 Some(&b"on"[..])
171 );
172 assert_eq!(
173 s.set("console.a-switch", b"off").unwrap().as_deref(),
174 Some(&b"on"[..]),
175 "the value it replaced"
176 );
177 assert_eq!(
178 s.get("console.a-switch").unwrap().as_deref(),
179 Some(&b"off"[..])
180 );
181 assert_eq!(s.set("x", &[0, 255, 7]).unwrap(), None);
182 assert_eq!(
183 s.get("x").unwrap().as_deref(),
184 Some(&[0u8, 255, 7][..]),
185 "any bytes"
186 );
187 }
188
189 #[test]
190 fn every_bound_is_refused_by_name() {
191 let s = store();
192 let words = |e: Error| e.to_string();
193 assert!(words(s.set("", b"").unwrap_err()).contains("empty"));
194 assert!(words(s.set(&"a".repeat(MAX_NAME_BYTES + 1), b"").unwrap_err()).contains("128"));
195 assert!(
196 s.set(&"a".repeat(MAX_NAME_BYTES), b"").is_ok(),
197 "at the bound it is a name"
198 );
199 assert!(words(s.set("Console.X", b"").unwrap_err()).contains("[a-z0-9._-]"));
200 assert!(words(s.set("a/b", b"").unwrap_err()).contains("[a-z0-9._-]"));
201 assert!(words(s.set(".hidden", b"").unwrap_err()).contains("start with"));
202 assert!(words(s.set("v", &vec![0; MAX_VALUE_BYTES + 1]).unwrap_err()).contains("4096"));
203 assert!(
204 s.set("v", &vec![0; MAX_VALUE_BYTES]).is_ok(),
205 "at the bound it is a value"
206 );
207 assert!(
208 words(s.get("A").unwrap_err()).contains("[a-z0-9._-]"),
209 "a read names the rule too"
210 );
211 }
212
213 #[test]
214 fn a_full_server_refuses_a_new_name_but_replaces_a_held_one() {
215 let s = store();
216 for i in 0..MAX_SETTINGS {
217 s.set(&format!("n{i}"), b"1").unwrap();
218 }
219 assert!(s
220 .set("one-more", b"1")
221 .unwrap_err()
222 .to_string()
223 .contains(&MAX_SETTINGS.to_string()));
224 assert!(s.set("n0", b"2").is_ok(), "a held name is still replaced");
225 }
226}