use std::path::Path;
pub fn shuffle<T>(items: &mut [T]) {
let Some(mut rng) = Rng::seeded() else {
return; };
for i in (1..items.len()).rev() {
items.swap(i, rng.below(i + 1));
}
}
pub struct Rng(u64);
impl Rng {
pub fn seeded() -> Option<Self> {
let mut seed = [0u8; 8];
getrandom::fill(&mut seed).ok()?;
Some(Self(u64::from_le_bytes(seed) | 1))
}
pub fn below(&mut self, n: usize) -> usize {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 % n as u64) as usize
}
}
pub fn truncate_bytes(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub fn sanitise_filename(s: &str) -> String {
let cleaned: String = s
.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
_ => c,
})
.collect::<String>()
.trim()
.to_string();
let cleaned = truncate_bytes(&cleaned, 240).trim_end().to_string();
match cleaned.as_str() {
"." | ".." => "_".into(),
_ => cleaned,
}
}
pub fn sanitise_extension(codec: &str) -> Option<String> {
let ext: String = codec
.chars()
.filter(char::is_ascii_alphanumeric)
.take(16)
.collect::<String>()
.to_lowercase();
(!ext.is_empty()).then_some(ext)
}
pub fn path_within(dir: &Path, path: &Path) -> bool {
path.strip_prefix(dir).is_ok_and(|rest| {
rest.components()
.all(|c| matches!(c, std::path::Component::Normal(_)))
})
}
pub fn year_of(date: &str) -> Option<&str> {
date.get(..4)
}
#[cfg(test)]
mod year_tests {
use super::year_of;
#[test]
fn a_year_is_the_first_four_characters_when_they_are_bytes_too() {
assert_eq!(year_of("1997-05-21"), Some("1997"));
assert_eq!(year_of("199"), None);
assert_eq!(year_of("1997"), None);
}
}