diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index 40ed2bed83..d1e2eacd5a 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -53,14 +53,13 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> { ), ); site.tera.register_function( - "get_file_hash", - global_fns::GetFileHash::new( + "get_hash", + global_fns::GetHash::new( site.base_path.clone(), site.config.theme.clone(), site.output_path.clone(), ), ); - site.tera.register_filter( "markdown", filters::MarkdownFilter::new( diff --git a/components/templates/src/global_fns/files.rs b/components/templates/src/global_fns/files.rs index 57c72f398a..13361c5dff 100644 --- a/components/templates/src/global_fns/files.rs +++ b/components/templates/src/global_fns/files.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::path::PathBuf; -use std::{fs, io, result}; +use std::fs; +use std::io::Read; use crate::global_fns::helpers::search_for_file; use config::Config; @@ -10,20 +11,20 @@ use libs::tera::{from_value, to_value, Function as TeraFn, Result, Value}; use libs::url; use utils::site::resolve_internal_link; -fn compute_file_hash( - mut file: fs::File, +fn compute_hash( + literal: String, as_base64: bool, -) -> result::Result +) -> String where digest::Output: core::fmt::LowerHex, D: std::io::Write, { let mut hasher = D::new(); - io::copy(&mut file, &mut hasher)?; + hasher.update(literal); if as_base64 { - Ok(encode_b64(hasher.finalize())) + encode_b64(hasher.finalize()) } else { - Ok(format!("{:x}", hasher.finalize())) + format!("{:x}", hasher.finalize()) } } @@ -128,7 +129,13 @@ impl TeraFn for GetUrl { ) .map_err(|e| format!("`get_url`: {}", e))? .and_then(|(p, _)| fs::File::open(&p).ok()) - .and_then(|f| compute_file_hash::(f, false).ok()) + .and_then(|mut f| { + let mut contents = String::new(); + + f.read_to_string(&mut contents).ok()?; + + Some(compute_hash::(contents, false)) + }) { Some(hash) => { permalink = format!("{}?h={}", permalink, hash); @@ -166,71 +173,99 @@ impl TeraFn for GetUrl { } #[derive(Debug)] -pub struct GetFileHash { +pub struct GetHash { base_path: PathBuf, theme: Option, output_path: PathBuf, } -impl GetFileHash { +impl GetHash { pub fn new(base_path: PathBuf, theme: Option, output_path: PathBuf) -> Self { Self { base_path, theme, output_path } } } -impl TeraFn for GetFileHash { +impl TeraFn for GetHash { fn call(&self, args: &HashMap) -> Result { - let path = required_arg!( + let path = optional_arg!( String, args.get("path"), - "`get_file_hash` requires a `path` argument with a string value" + "`get_hash` requires either a `path` or a `literal` argument with a string value" + ); + + let literal = optional_arg!( + String, + args.get("literal"), + "`get_hash` requires either a `path` or a `literal` argument with a string value" ); + + let contents = match (path, literal) { + (Some(_), Some(_)) => { + return Err("`get_hash`: must have only one of `path` or `literal` argument".into()); + }, + (None, None) => { + return Err("`get_hash`: must have at least one of `path` or `literal` argument".into()); + }, + (Some(path_v), None) => { + let file_path = + match search_for_file(&self.base_path, &path_v, &self.theme, &self.output_path) + .map_err(|e| format!("`get_hash`: {}", e))? + { + Some((f, _)) => f, + None => { + return Err(format!("`get_hash`: Cannot find file: {}", path_v).into()); + } + }; + + let mut f = match std::fs::File::open(file_path) { + Ok(f) => f, + Err(e) => { + return Err(format!("File {} could not be open: {}", path_v, e).into()); + } + }; + + let mut contents = String::new(); + + match f.read_to_string(&mut contents) { + Ok(f) => f, + Err(e) => { + return Err(format!("File {} could not be read: {}", path_v, e).into()); + } + }; + + contents + } + (None, Some(literal_v)) => { literal_v } + }; + + let sha_type = optional_arg!( u16, args.get("sha_type"), - "`get_file_hash`: `sha_type` must be 256, 384 or 512" + "`get_hash`: `sha_type` must be 256, 384 or 512" ) .unwrap_or(384); + let base64 = optional_arg!( bool, args.get("base64"), - "`get_file_hash`: `base64` must be true or false" + "`get_hash`: `base64` must be true or false" ) .unwrap_or(true); - let file_path = - match search_for_file(&self.base_path, &path, &self.theme, &self.output_path) - .map_err(|e| format!("`get_file_hash`: {}", e))? - { - Some((f, _)) => f, - None => { - return Err(format!("`get_file_hash`: Cannot find file: {}", path).into()); - } - }; - - let f = match std::fs::File::open(file_path) { - Ok(f) => f, - Err(e) => { - return Err(format!("File {} could not be open: {}", path, e).into()); - } - }; - let hash = match sha_type { - 256 => compute_file_hash::(f, base64), - 384 => compute_file_hash::(f, base64), - 512 => compute_file_hash::(f, base64), - _ => return Err("`get_file_hash`: Invalid sha value".into()), + 256 => compute_hash::(contents, base64), + 384 => compute_hash::(contents, base64), + 512 => compute_hash::(contents, base64), + _ => return Err("`get_hash`: Invalid sha value".into()), }; - match hash { - Ok(digest) => Ok(to_value(digest).unwrap()), - Err(_) => Err("`get_file_hash`: could no compute hash".into()), - } + Ok(to_value(hash).unwrap()) } } #[cfg(test)] mod tests { - use super::{GetFileHash, GetUrl}; + use super::{GetHash, GetUrl}; use std::collections::HashMap; use std::fs::create_dir; @@ -456,7 +491,7 @@ title = "A title" #[test] fn can_get_file_hash_sha256_no_base64() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("sha_type".to_string(), to_value(256).unwrap()); @@ -470,7 +505,7 @@ title = "A title" #[test] fn can_get_file_hash_sha256_base64() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("sha_type".to_string(), to_value(256).unwrap()); @@ -481,7 +516,7 @@ title = "A title" #[test] fn can_get_file_hash_sha384_no_base64() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("base64".to_string(), to_value(false).unwrap()); @@ -494,7 +529,7 @@ title = "A title" #[test] fn can_get_file_hash_sha384() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); assert_eq!( @@ -506,7 +541,7 @@ title = "A title" #[test] fn can_get_file_hash_sha512_no_base64() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("sha_type".to_string(), to_value(512).unwrap()); @@ -520,7 +555,7 @@ title = "A title" #[test] fn can_get_file_hash_sha512() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("sha_type".to_string(), to_value(512).unwrap()); @@ -530,6 +565,85 @@ title = "A title" ); } + #[test] + fn can_get_hash_sha256_no_base64() { + let dir = create_temp_dir(); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); + let mut args = HashMap::new(); + args.insert("literal".to_string(), to_value("Hello World").unwrap()); + args.insert("sha_type".to_string(), to_value(256).unwrap()); + args.insert("base64".to_string(), to_value(false).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" + ); + } + + #[test] + fn can_get_hash_sha256_base64() { + let dir = create_temp_dir(); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); + let mut args = HashMap::new(); + args.insert("literal".to_string(), to_value("Hello World").unwrap()); + args.insert("sha_type".to_string(), to_value(256).unwrap()); + args.insert("base64".to_string(), to_value(true).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4="); + } + + #[test] + fn can_get_hash_sha384_no_base64() { + let dir = create_temp_dir(); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); + let mut args = HashMap::new(); + args.insert("literal".to_string(), to_value("Hello World").unwrap()); + args.insert("base64".to_string(), to_value(false).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "99514329186b2f6ae4a1329e7ee6c610a729636335174ac6b740f9028396fcc803d0e93863a7c3d90f86beee782f4f3f" + ); + } + + #[test] + fn can_get_hash_sha384() { + let dir = create_temp_dir(); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); + let mut args = HashMap::new(); + args.insert("literal".to_string(), to_value("Hello World").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "mVFDKRhrL2rkoTKefubGEKcpY2M1F0rGt0D5AoOW/MgD0Ok4Y6fD2Q+Gvu54L08/" + ); + } + + #[test] + fn can_get_hash_sha512_no_base64() { + let dir = create_temp_dir(); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); + let mut args = HashMap::new(); + args.insert("literal".to_string(), to_value("Hello World").unwrap()); + args.insert("sha_type".to_string(), to_value(512).unwrap()); + args.insert("base64".to_string(), to_value(false).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "2c74fd17edafd80e8447b0d46741ee243b7eb74dd2149a0ab1b9246fb30382f27e853d8585719e0e67cbda0daa8f51671064615d645ae27acb15bfb1447f459b" + ); + } + + #[test] + fn can_get_hash_sha512() { + let dir = create_temp_dir(); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); + let mut args = HashMap::new(); + args.insert("literal".to_string(), to_value("Hello World").unwrap()); + args.insert("sha_type".to_string(), to_value(512).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "LHT9F+2v2A6ER7DUZ0HuJDt+t03SFJoKsbkkb7MDgvJ+hT2FhXGeDmfL2g2qj1FnEGRhXWRa4nrLFb+xRH9Fmw==" + ); + } + #[test] fn can_resolve_asset_path_to_valid_url() { let config = Config::parse(CONFIG_DATA).unwrap(); @@ -540,7 +654,7 @@ title = "A title" args.insert( "path".to_string(), to_value(dir.path().join("app.css").strip_prefix(std::env::temp_dir()).unwrap()) - .unwrap(), + .unwrap(), ); assert_eq!( static_fn.call(&args).unwrap(), @@ -554,7 +668,7 @@ title = "A title" #[test] fn error_when_file_not_found_for_hash() { let dir = create_temp_dir(); - let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new()); + let static_fn = GetHash::new(dir.into_path(), None, PathBuf::new()); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("doesnt-exist").unwrap()); let err = format!("{}", static_fn.call(&args).unwrap_err()); diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 25a94c5582..154dc266b6 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -9,7 +9,7 @@ mod images; mod load_data; pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyTerm, GetTaxonomyUrl}; -pub use self::files::{GetFileHash, GetUrl}; +pub use self::files::{GetHash, GetUrl}; pub use self::i18n::Trans; pub use self::images::{GetImageMetadata, ResizeImage}; pub use self::load_data::LoadData; diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md index ec446915fe..6cf49bbdc9 100644 --- a/docs/content/documentation/templates/overview.md +++ b/docs/content/documentation/templates/overview.md @@ -234,17 +234,21 @@ In the case of non-internal links, you can also add a cachebust of the format `? by passing `cachebust=true` to the `get_url` function. In this case, the path will need to resolve to an actual file. See [File Searching Logic](@/documentation/templates/overview.md#file-searching-logic) for details. -### `get_file_hash` +### `get_hash` -Returns the hash digest (SHA-256, SHA-384 or SHA-512) of a file. +Returns the hash digest (SHA-256, SHA-384 or SHA-512) of a file or a string literal. It can take the following arguments: - `path`: mandatory, see [File Searching Logic](@/documentation/templates/overview.md#file-searching-logic) for details +- **or** `literal`: mandatory, the string value to be hashed - `sha_type`: optional, one of `256`, `384` or `512`, defaults to `384` - `base64`: optional, `true` or `false`, defaults to `true`. Whether to encode the hash as base64 +Either `path` or `literal` must be given. + ```jinja2 -{{/* get_file_hash(path="static/js/app.js", sha_type=256) */}} +{{/* get_hash(literal="Hello World", sha_type=256) */}} +{{/* get_hash(path="static/js/app.js", sha_type=256) */}} ``` The function can also output a base64-encoded hash value when its `base64` @@ -253,10 +257,10 @@ integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Int ```jinja2 + integrity="sha384-{{ get_hash(path="static/js/app.js", sha_type=384, base64=true) | safe }}"> ``` -Do note that subresource integrity is typically used when using external scripts, which `get_file_hash` does not support. +Do note that subresource integrity is typically used when using external scripts, which `get_hash` does not support. ### `get_image_metadata` diff --git a/test_site/templates/index.html b/test_site/templates/index.html index 35a24be4b7..0c19bb0867 100644 --- a/test_site/templates/index.html +++ b/test_site/templates/index.html @@ -14,5 +14,5 @@

{{ page.title }}

{% block script %} + integrity="sha384-{{ get_hash(path="scripts/hello.js", base64=true) | safe }}"> {% endblock script %}