gled-model 2.31.2

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! Pictures in the asset repository: layout backgrounds and photos of
//! fixtures. Projects and definitions name a picture by its path in the
//! repository, so every machine with the repository finds it, and
//! interfaces fetch it from the engine instead of reading a disk.

use std::path::{Component, Path, PathBuf};

use uuid::Uuid;

/// Where pictures chosen in an interface are kept.
pub const PICTURES_DIR: &str = "pictures";

/// The kinds of pictures gled draws: bitmaps and drawings.
pub const EXTENSIONS: [&str; 4] = ["png", "jpg", "jpeg", "svg"];

/// Whether `path` names a picture inside the repository: relative, without
/// leaving it, and of a kind gled draws. The engine hands out and stores
/// only such files.
pub fn is_picture_path(path: &Path) -> bool {
    let inside = path
        .components()
        .all(|part| matches!(part, Component::Normal(_)));
    let picture = path.extension().is_some_and(|extension| {
        EXTENSIONS
            .iter()
            .any(|known| extension.eq_ignore_ascii_case(known))
    });
    inside && picture && path.components().next().is_some()
}

/// A path for a new picture of the kind `extension` names, under
/// `PICTURES_DIR`, unique so it never replaces another.
pub fn new_picture_path(extension: &str) -> PathBuf {
    Path::new(PICTURES_DIR).join(format!(
        "{}.{}",
        Uuid::new_v4(),
        extension.to_ascii_lowercase()
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pictures_inside_the_repository_are_accepted() {
        assert!(is_picture_path(Path::new("backgrounds/stage.svg")));
        assert!(is_picture_path(Path::new("pictures/lamp.PNG")));
    }

    #[test]
    fn paths_leaving_the_repository_are_refused() {
        assert!(!is_picture_path(Path::new("/etc/stage.png")));
        assert!(!is_picture_path(Path::new("../stage.png")));
        assert!(!is_picture_path(Path::new("pictures/../../stage.png")));
    }

    #[test]
    fn files_that_are_no_pictures_are_refused() {
        assert!(!is_picture_path(Path::new("projects/show.json")));
        assert!(!is_picture_path(Path::new("pictures/stage")));
    }

    #[test]
    fn a_new_picture_is_named_for_its_kind() {
        let path = new_picture_path("JPG");
        assert!(path.starts_with(PICTURES_DIR));
        assert_eq!(path.extension().and_then(|e| e.to_str()), Some("jpg"));
        assert!(is_picture_path(&path));
    }
}