Skip to main content

gumbo_lib/
javascript.rs

1use crate::errors::Result;
2use minifier::js::minify;
3use sha3::{Digest, Sha3_256};
4use std::path::{Path, PathBuf};
5
6#[derive(Clone)]
7
8/// This special wrapper used to verify the contents of JS files.
9///
10/// This struct is responsible for reading JS file from ./assets/js/*
11/// and verifying their contents match their hash when returned
12pub struct JsFile {
13    hash: String,
14    content: String,
15}
16
17impl JsFile {
18    pub fn new(filename: &str) -> Result<JsFile> {
19        let dir = PathBuf::new().join("./src/assets/js");
20        let path = dir.clone().join(format!("{filename}.js"));
21
22        // Make SURE the file is within the ./assets/js directory
23        if !is_within_directory(&dir, &path) {
24            let kind = std::io::ErrorKind::NotFound;
25            Err(std::io::Error::new(
26                kind,
27                "File is not within the specified directory",
28            ))?;
29        }
30
31        let content = std::fs::read_to_string(path)?;
32
33        let mut hasher = Sha3_256::new();
34        hasher.update(content.as_bytes());
35        let hash = hasher.finalize();
36        let hex_hash = base16ct::lower::encode_string(&hash);
37        Ok(JsFile {
38            hash: hex_hash,
39            content,
40        })
41    }
42
43    pub fn contents(&self) -> &str {
44        &self.content
45    }
46
47    pub fn min_contents(self) -> String {
48        minify(&self.content).to_string()
49    }
50
51    pub fn verify_hash(&self, hash: &str) -> Result<&Self> {
52        if !self.hash.starts_with(hash) || hash.is_empty() {
53            Err(std::io::Error::other("File Hash Invalid"))?;
54        }
55        Ok(self)
56    }
57}
58
59fn is_within_directory(dir: &Path, filename: &Path) -> bool {
60    // Get the absolute canonical paths of both `dir` and `filename`
61    if let (Ok(dir_abs), Ok(filename_abs)) = (dir.canonicalize(), filename.canonicalize()) {
62        // Check if `filename_abs` starts with `dir_abs`, meaning it's inside the directory
63        return filename_abs.starts_with(&dir_abs);
64    }
65    false
66}
67
68/// Returns the URL to the request javascript file.
69/// This URL contains the JS file's hashed,
70/// so when JS files change, new versions are served up
71pub fn js_path(filename: &str) -> Result<String> {
72    js_path_absolute(filename)
73}
74
75/// Returns the URL to the request javascript file.
76/// Path is relative
77/// This URL contains the JS file's hashed,
78/// so when JS files change, new versions are served up
79pub fn js_path_relative(filename: &str) -> Result<String> {
80    let file = JsFile::new(filename)?;
81    let hash = &file.hash[0..10];
82    Ok(format!("assets/js/{filename}-{hash}.js"))
83}
84
85/// Returns the URL to the request javascript file.
86/// Path is absolute
87/// This URL contains the JS file's hashed,
88/// so when JS files change, new versions are served up
89pub fn js_path_absolute(filename: &str) -> Result<String> {
90    let root = super::app_root();
91    let file = JsFile::new(filename)?;
92    let hash = &file.hash[0..10];
93    Ok(format!("{root}assets/js/{filename}-{hash}.js"))
94}