const CHUNK: usize = 1800;
const MARK: &str = "aura-chunks:";
fn slot(account: &str, index: usize) -> String {
if index == 0 { account.into() } else { format!("{account}#{index}") }
}
fn split(value: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut rest = value;
while rest.len() > CHUNK {
let cut = (0..=CHUNK).rev().find(|&at| rest.is_char_boundary(at)).unwrap_or(CHUNK);
let (head, tail) = rest.split_at(cut);
parts.push(head);
rest = tail;
}
parts.push(rest);
parts
}
pub fn set(service: &str, account: &str, value: &str) -> Result<(), String> {
if service.chars().chain(account.chars()).any(char::is_control) {
return Err("el servicio y la cuenta no pueden contener caracteres de control".into());
}
let parts = split(value);
for (index, part) in parts.iter().enumerate() {
let text = if parts.len() > 1 && index == 0 { format!("{MARK}{}:{part}", parts.len()) } else { (*part).into() };
super::sys::secret_set(service, &slot(account, index), &text)?;
}
remove_from(service, account, parts.len())
}
pub fn get(service: &str, account: &str) -> Option<String> {
let first = super::sys::secret_get(service, account)?;
let Some(header) = first.strip_prefix(MARK) else { return Some(first) };
let (count, head) = header.split_once(':')?;
let mut value = String::from(head);
for index in 1..count.parse::<usize>().ok()? {
value.push_str(&super::sys::secret_get(service, &slot(account, index))?);
}
Some(value)
}
pub fn remove(service: &str, account: &str) -> Result<(), String> {
super::sys::secret_remove(service, account)?;
remove_from(service, account, 1)
}
fn remove_from(service: &str, account: &str, first: usize) -> Result<(), String> {
let mut index = first;
while super::sys::secret_get(service, &slot(account, index)).is_some() {
super::sys::secret_remove(service, &slot(account, index))?;
index += 1;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_valor_largo_se_parte_sin_cortar_caracteres_y_se_une_igual() {
let long = "ñé€".repeat(2000);
let parts = split(&long);
assert!(parts.len() > 1 && parts.iter().all(|part| part.len() <= CHUNK));
assert_eq!(parts.concat(), long);
assert_eq!(split("corto"), ["corto"]);
assert_eq!(split(""), [""]);
}
#[test]
fn rechaza_caracteres_de_control_en_los_nombres() {
assert!(set("aura\n", "x", "v").is_err());
assert!(set("aura", "x\0", "v").is_err());
}
#[test]
#[ignore]
fn un_valor_largo_y_con_comillas_ida_y_vuelta() {
let service = format!("aura-test-{}", std::process::id());
let long = "tok\u{e9}n\"&'\\$`".repeat(300);
set(&service, "prueba", &long).unwrap();
assert_eq!(get(&service, "prueba").as_deref(), Some(long.as_str()));
set(&service, "prueba", "corto").unwrap();
assert_eq!(get(&service, "prueba").as_deref(), Some("corto"));
remove(&service, "prueba").unwrap();
assert_eq!(get(&service, "prueba"), None);
remove(&service, "prueba").unwrap();
}
}