use std::sync::Arc;
use crate::control::ControlStore;
use crate::doc::{impl_doc, DocRef, ShardedMap};
use crate::error::{Error, Result};
pub const SETTINGS_REF: &str = "refs/gunnar/settings";
pub const MAX_NAME_BYTES: usize = 128;
pub const MAX_VALUE_BYTES: usize = 4096;
pub const MAX_SETTINGS: usize = 256;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct SettingsDoc {
#[serde(default)]
values: ShardedMap<String>,
}
impl_doc!(SettingsDoc { values });
#[derive(Clone, Debug)]
pub struct SettingsStore {
doc: DocRef<SettingsDoc>,
}
pub fn check_name(name: &str) -> Result<()> {
let refuse = |reason: String| {
Err(Error::InvalidInput {
what: "setting name",
reason,
})
};
if name.is_empty() {
return refuse("empty".to_owned());
}
if name.len() > MAX_NAME_BYTES {
return refuse(format!(
"{} bytes, more than the {MAX_NAME_BYTES} a name may have",
name.len()
));
}
if !name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-'))
{
return refuse(format!("{name:?} has a character outside [a-z0-9._-]"));
}
if !name.as_bytes()[0].is_ascii_alphanumeric() {
return refuse(format!("{name:?} does not start with a letter or a digit"));
}
Ok(())
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex(s: &str) -> Result<Vec<u8>> {
let bad = || Error::InvalidInput {
what: "stored setting",
reason: "not hex: the settings document is damaged".to_owned(),
};
if s.len() % 2 != 0 {
return Err(bad());
}
(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()
}
impl SettingsStore {
pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
Ok(SettingsStore {
doc: DocRef::new(store, SETTINGS_REF)?,
})
}
pub fn get(&self, name: &str) -> Result<Option<Vec<u8>>> {
check_name(name)?;
self.doc
.load()?
.values
.get(name)?
.map(|v| unhex(&v))
.transpose()
}
pub fn set(&self, name: &str, value: &[u8]) -> Result<Option<Vec<u8>>> {
check_name(name)?;
if value.len() > MAX_VALUE_BYTES {
return Err(Error::InvalidInput {
what: "setting value",
reason: format!(
"{} bytes, more than the {MAX_VALUE_BYTES} a value may have",
value.len()
),
});
}
let previous = self.doc.update(|doc| {
if !doc.values.contains_key(name)? && doc.values.len() >= MAX_SETTINGS {
return Err(Error::InvalidInput {
what: "setting",
reason: format!(
"this server holds {MAX_SETTINGS} settings already, the most it keeps"
),
});
}
doc.values.insert(name.to_owned(), hex(value))
})?;
previous.map(|v| unhex(&v)).transpose()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn store() -> SettingsStore {
SettingsStore::new(Arc::new(
crate::control::RedbControlStore::in_memory().expect("a store"),
))
.expect("the document")
}
#[test]
fn a_setting_is_stored_replaced_and_read_back_and_an_unset_one_is_none() {
let s = store();
assert_eq!(s.get("console.a-switch").unwrap(), None);
assert_eq!(
s.set("console.a-switch", b"on").unwrap(),
None,
"a new name replaced nothing"
);
assert_eq!(
s.get("console.a-switch").unwrap().as_deref(),
Some(&b"on"[..])
);
assert_eq!(
s.set("console.a-switch", b"off").unwrap().as_deref(),
Some(&b"on"[..]),
"the value it replaced"
);
assert_eq!(
s.get("console.a-switch").unwrap().as_deref(),
Some(&b"off"[..])
);
assert_eq!(s.set("x", &[0, 255, 7]).unwrap(), None);
assert_eq!(
s.get("x").unwrap().as_deref(),
Some(&[0u8, 255, 7][..]),
"any bytes"
);
}
#[test]
fn every_bound_is_refused_by_name() {
let s = store();
let words = |e: Error| e.to_string();
assert!(words(s.set("", b"").unwrap_err()).contains("empty"));
assert!(words(s.set(&"a".repeat(MAX_NAME_BYTES + 1), b"").unwrap_err()).contains("128"));
assert!(
s.set(&"a".repeat(MAX_NAME_BYTES), b"").is_ok(),
"at the bound it is a name"
);
assert!(words(s.set("Console.X", b"").unwrap_err()).contains("[a-z0-9._-]"));
assert!(words(s.set("a/b", b"").unwrap_err()).contains("[a-z0-9._-]"));
assert!(words(s.set(".hidden", b"").unwrap_err()).contains("start with"));
assert!(words(s.set("v", &vec![0; MAX_VALUE_BYTES + 1]).unwrap_err()).contains("4096"));
assert!(
s.set("v", &vec![0; MAX_VALUE_BYTES]).is_ok(),
"at the bound it is a value"
);
assert!(
words(s.get("A").unwrap_err()).contains("[a-z0-9._-]"),
"a read names the rule too"
);
}
#[test]
fn a_full_server_refuses_a_new_name_but_replaces_a_held_one() {
let s = store();
for i in 0..MAX_SETTINGS {
s.set(&format!("n{i}"), b"1").unwrap();
}
assert!(s
.set("one-more", b"1")
.unwrap_err()
.to_string()
.contains(&MAX_SETTINGS.to_string()));
assert!(s.set("n0", b"2").is_ok(), "a held name is still replaced");
}
}