nano-web 1.4.2

Static file server built with Rust with pre-compressed in-memory caching
Documentation
use anyhow::Result;
use minijinja::{context, Environment};
use std::collections::HashMap;
use std::env;

pub fn render_template(content: &str, config_prefix: &str) -> Result<String> {
    let jinja = Environment::new();

    // Collect environment variables with the specified prefix
    let mut env_vars = HashMap::new();
    for (key, value) in env::vars() {
        if key.starts_with(config_prefix) {
            let trimmed_key = key.strip_prefix(config_prefix).unwrap();
            env_vars.insert(trimmed_key.to_string(), value);
        }
    }

    let json_string = serde_json::to_string(&env_vars)?;
    let escaped_json = json_string.replace('"', "\\\"");

    // MiniJinja has better syntax and is more secure by default
    let template = jinja.template_from_str(content)?;

    template
        .render(context! {
            env => env_vars,
            Json => json_string,
            EscapedJson => escaped_json
        })
        .map_err(|e| anyhow::anyhow!("Template rendering error: {e}"))
}

#[cfg(test)]
#[allow(unsafe_code)]
mod tests {
    use super::*;
    use std::env;

    // SAFETY: env::set_var is unsafe since Rust 1.66 in multi-threaded contexts.
    // These tests run serially (cargo test default) and each cleans up after itself.

    #[test]
    fn test_template_rendering_with_env_vars() {
        // SAFETY: single-threaded test context
        unsafe {
            env::set_var("VITE_API_URL", "http://localhost:3000");
            env::set_var("VITE_APP_NAME", "Test App");
            env::set_var("OTHER_VAR", "should_not_appear");
        }

        let template = r#"
        <script>
            window.ENV = JSON.parse("{{EscapedJson}}");
            window.API_URL = "{{env.API_URL}}";
        </script>
        "#;

        let result = render_template(template, "VITE_").unwrap();

        assert!(result.contains("API_URL"));
        assert!(result.contains("Test App"));
        assert!(!result.contains("should_not_appear"));
        assert!(result.contains("JSON.parse"));

        unsafe {
            env::remove_var("VITE_API_URL");
            env::remove_var("VITE_APP_NAME");
            env::remove_var("OTHER_VAR");
        }
    }

    #[test]
    fn test_template_with_no_env_vars() {
        let template = r"
        <script>
            window.ENV = {{Json}};
        </script>
        ";

        let result = render_template(template, "NONEXISTENT_").unwrap();
        assert!(result.contains("{}"));
    }

    #[test]
    fn test_template_with_special_characters() {
        unsafe {
            env::set_var("TEST_QUOTES", r#"value with "quotes""#);
            env::set_var("TEST_SLASHES", "path/to/file");
        }

        let template = r#"<script>window.ENV = JSON.parse("{{EscapedJson}}");</script>"#;

        let result = render_template(template, "TEST_").unwrap();
        assert!(result.contains("quotes"));
        assert!(result.contains("path/to/file"));

        unsafe {
            env::remove_var("TEST_QUOTES");
            env::remove_var("TEST_SLASHES");
        }
    }

    #[test]
    fn test_template_invalid_syntax() {
        let template = "{{invalid.template.syntax}}";
        assert!(render_template(template, "VITE_").is_err());
    }

    #[test]
    fn test_different_prefixes() {
        unsafe {
            env::set_var("REACT_APP_URL", "react-url");
            env::set_var("VUE_APP_URL", "vue-url");
        }

        let template = "{{env.APP_URL}}";

        let react_result = render_template(template, "REACT_").unwrap();
        let vue_result = render_template(template, "VUE_").unwrap();

        assert!(react_result.contains("react-url"));
        assert!(vue_result.contains("vue-url"));

        unsafe {
            env::remove_var("REACT_APP_URL");
            env::remove_var("VUE_APP_URL");
        }
    }
}