use std::{
fs,
path::{Component, Path, PathBuf},
};
use std::time::Instant;
const RESCAN: std::time::Duration = std::time::Duration::from_secs(2);
#[derive(Debug, Default)]
pub(super) struct SvgTemplates {
names: Vec<String>,
scanned: Option<Instant>,
}
impl SvgTemplates {
pub(super) fn names(&mut self) -> Vec<String> {
if self
.scanned
.is_none_or(|scanned| scanned.elapsed() >= RESCAN)
{
self.scanned = Some(Instant::now());
let root = folder();
let mut names = Vec::new();
collect(&root, &root, &mut names);
names.sort();
self.names = names;
}
self.names.clone()
}
}
pub(super) fn read(name: &str) -> Result<String, String> {
let relative = Path::new(name);
if relative
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(format!("{name} is not a template"));
}
fs::read_to_string(folder().join(relative))
.map_err(|err| format!("Could not read the SVG template {name}: {err}"))
}
fn folder() -> PathBuf {
crate::storage::STORAGE_DIR.join("svg")
}
fn collect(root: &Path, dir: &Path, names: &mut Vec<String>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect(root, &path, names);
} else if path.extension().is_some_and(|ext| ext == "svg")
&& let Ok(relative) = path.strip_prefix(root)
{
names.push(relative.to_string_lossy().replace('\\', "/"));
}
}
}
#[cfg(test)]
mod tests {
use super::read;
#[test]
fn names_cannot_leave_the_folder() {
assert!(read("../projects/secret.json").is_err());
assert!(read("/etc/passwd").is_err());
assert!(read("Photonenkollektiv/../../x.svg").is_err());
}
}