pacenotes_server/store/
motd.rs1pub const MOTD_DIR: &str = "motd";
3
4pub const MOTD_FALLBACK_LANG: &str = "en";
5
6pub fn valid_lang(lang: &str) -> bool {
9 (2..=8).contains(&lang.len()) && lang.bytes().all(|b| b.is_ascii_lowercase())
10}
11
12pub fn read_motd(data_dir: &std::path::Path, lang: &str) -> Option<String> {
14 let mut candidates: Vec<&str> = Vec::new();
15 if valid_lang(lang) {
16 candidates.push(lang);
17 } else if !lang.is_empty() {
18 tracing::debug!("ignoring motd lang request: not [a-z]{{2,8}}");
19 }
20 if !candidates.contains(&MOTD_FALLBACK_LANG) {
21 candidates.push(MOTD_FALLBACK_LANG);
22 }
23
24 candidates.into_iter().find_map(|candidate| {
25 let path = data_dir.join(MOTD_DIR).join(format!("{candidate}.txt"));
26 match std::fs::read_to_string(&path) {
27 Ok(text) => Some(text),
28 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
29 Err(e) => {
30 tracing::warn!(error = %e, path = %path.display(), "reading motd failed");
31 None
32 }
33 }
34 })
35}
36
37#[cfg(test)]
38mod tests {
39 #[test]
40 fn motd_falls_back_to_english_and_refuses_traversal() {
41 let tmp = tempfile::tempdir().unwrap();
42 let dir = tmp.path();
43 assert!(super::read_motd(dir, "pl").is_none());
44
45 crate::store::fixture_tree::write(&dir.join("motd/en.txt"), b"English news");
46 assert_eq!(super::read_motd(dir, "en").as_deref(), Some("English news"));
47 assert_eq!(super::read_motd(dir, "pl").as_deref(), Some("English news"));
48 for odd in ["", "../manifest", "en/../en"] {
49 assert_eq!(
50 super::read_motd(dir, odd).as_deref(),
51 Some("English news"),
52 "{odd:?}"
53 );
54 }
55
56 crate::store::fixture_tree::write(&dir.join("motd/pl.txt"), "Polskie wieści".as_bytes());
57 assert_eq!(
58 super::read_motd(dir, "pl").as_deref(),
59 Some("Polskie wieści")
60 );
61 assert_eq!(super::read_motd(dir, "en").as_deref(), Some("English news"));
62 }
63
64 #[test]
65 fn lang_validation() {
66 for good in ["en", "pt", "ptbr"] {
67 assert!(super::valid_lang(good), "{good:?}");
68 }
69 for bad in ["e", "", "EN", "en-GB", "..", "toolonglanguagetag"] {
70 assert!(!super::valid_lang(bad), "{bad:?}");
71 }
72 }
73}