use crate::{Asset, AssetCertificationError};
use globset::{Glob, GlobMatcher};
use ic_http_certification::StatusCode;
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
pub enum AssetConfig {
File {
path: String,
content_type: Option<String>,
headers: Vec<(String, String)>,
fallback_for: Vec<AssetFallbackConfig>,
aliased_by: Vec<String>,
encodings: Vec<(AssetEncoding, String)>,
},
Pattern {
pattern: String,
content_type: Option<String>,
headers: Vec<(String, String)>,
encodings: Vec<(AssetEncoding, String)>,
},
Redirect {
from: String,
to: String,
kind: AssetRedirectKind,
headers: Vec<(String, String)>,
},
}
#[derive(Debug, Clone)]
pub struct AssetFallbackConfig {
pub scope: String,
pub status_code: Option<StatusCode>,
}
#[derive(Debug, Clone)]
pub enum AssetRedirectKind {
Permanent,
Temporary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AssetEncoding {
Identity,
Brotli,
Zstd,
Gzip,
Deflate,
}
impl AssetEncoding {
pub fn default_config(self) -> (AssetEncoding, String) {
let file_extension = match self {
AssetEncoding::Identity => "".to_string(),
AssetEncoding::Brotli => ".br".to_string(),
AssetEncoding::Zstd => ".zst".to_string(),
AssetEncoding::Gzip => ".gz".to_string(),
AssetEncoding::Deflate => ".zz".to_string(),
};
(self, file_extension)
}
pub fn custom_config(self, extension: String) -> (AssetEncoding, String) {
(self, extension)
}
}
impl Display for AssetEncoding {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let str = match self {
AssetEncoding::Identity => "identity".to_string(),
AssetEncoding::Brotli => "br".to_string(),
AssetEncoding::Zstd => "zstd".to_string(),
AssetEncoding::Gzip => "gzip".to_string(),
AssetEncoding::Deflate => "deflate".to_string(),
};
write!(f, "{str}")
}
}
#[derive(Debug, Clone)]
pub(crate) enum NormalizedAssetConfig {
File {
path: String,
content_type: Option<String>,
headers: Vec<(String, String)>,
fallback_for: Vec<AssetFallbackConfig>,
aliased_by: Vec<String>,
encodings: Vec<(AssetEncoding, String)>,
},
Pattern {
pattern: GlobMatcher,
content_type: Option<String>,
headers: Vec<(String, String)>,
encodings: Vec<(AssetEncoding, String)>,
},
Redirect {
from: String,
to: String,
kind: AssetRedirectKind,
headers: Vec<(String, String)>,
},
}
impl TryFrom<AssetConfig> for NormalizedAssetConfig {
type Error = AssetCertificationError;
fn try_from(config: AssetConfig) -> Result<Self, Self::Error> {
match config {
AssetConfig::File {
path,
content_type,
headers,
fallback_for,
aliased_by,
encodings,
} => Ok(NormalizedAssetConfig::File {
path,
content_type,
headers,
fallback_for,
aliased_by,
encodings,
}),
AssetConfig::Pattern {
pattern,
content_type,
headers,
encodings,
} => Ok(NormalizedAssetConfig::Pattern {
pattern: Glob::new(&pattern)?.compile_matcher(),
content_type,
headers,
encodings,
}),
AssetConfig::Redirect {
from,
to,
kind,
headers,
} => Ok(NormalizedAssetConfig::Redirect {
from,
to,
kind,
headers,
}),
}
}
}
impl NormalizedAssetConfig {
pub(crate) fn matches_asset(&self, asset: &Asset) -> bool {
match self {
Self::File { path, .. } => path == asset.path.as_ref(),
Self::Pattern { pattern, .. } => pattern.is_match(asset.path.as_ref()),
Self::Redirect { .. } => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Asset;
use rstest::*;
#[rstest]
#[case("index.html", "index.html", true)]
#[case("app.js", "app.js", true)]
#[case("index.js", "app.css", false)]
#[case("index.css", "index.js", false)]
fn matches_asset_file(
#[case] asset_path: &str,
#[case] config_path: &str,
#[case] expected: bool,
) {
let asset = Asset::new(asset_path, vec![]);
let config: NormalizedAssetConfig = AssetConfig::File {
path: config_path.to_string(),
content_type: None,
headers: vec![],
fallback_for: vec![],
aliased_by: vec![],
encodings: vec![],
}
.try_into()
.unwrap();
assert_eq!(config.matches_asset(&asset), expected);
}
#[rstest]
#[case("index.html", "*", true)]
#[case("index.html", "**", true)]
#[case("index.html", "**/*", true)]
#[case("index.html", "**/**", true)]
#[case("app.js", "*", true)]
#[case("app.js", "**", true)]
#[case("app.js", "**/*", true)]
#[case("app.js", "**/**", true)]
#[case("index.html", "*.html", true)]
#[case("index.html", "**.html", true)]
#[case("index.html", "**/*.html", true)]
#[case("index.html", "**/**.html", true)]
#[case("app.js", "*.html", false)]
#[case("app.js", "**.html", false)]
#[case("app.js", "**/*.html", false)]
#[case("app.js", "**/**.html", false)]
#[case("app.js", "*.js", true)]
#[case("app.js", "**.js", true)]
#[case("app.js", "**/*.js", true)]
#[case("app.js", "**/**.js", true)]
#[case("index.html", "*.{js,html}", true)]
#[case("index.html", "**.{js,html}", true)]
#[case("index.html", "**/*.{js,html}", true)]
#[case("index.html", "**/**.{js,html}", true)]
#[case("app.js", "*.{js,html}", true)]
#[case("app.js", "**.{js,html}", true)]
#[case("app.js", "**/*.{js,html}", true)]
#[case("app.js", "**/**.{js,html}", true)]
#[case("index.html", "assets/*.html", false)]
#[case("index.html", "assets/**.html", false)]
#[case("index.html", "assets/**/*.html", false)]
#[case("app.js", "assets/*.js", false)]
#[case("app.js", "assets/**.js", false)]
#[case("app.js", "assets/**/*.js", false)]
#[case("assets/index.html", "*", true)]
#[case("assets/index.html", "**", true)]
#[case("assets/index.html", "**/*", true)]
#[case("assets/index.html", "**/**", true)]
#[case("assets/app.js", "*", true)]
#[case("assets/app.js", "**", true)]
#[case("assets/app.js", "**/*", true)]
#[case("assets/app.js", "**/**", true)]
#[case("assets/index.html", "*.html", true)]
#[case("assets/index.html", "**.html", true)]
#[case("assets/index.html", "**/*.html", true)]
#[case("assets/app.js", "*.js", true)]
#[case("assets/app.js", "**.js", true)]
#[case("assets/app.js", "**/*.js", true)]
#[case("assets/index.html", "assets/*.html", true)]
#[case("assets/index.html", "assets/**.html", true)]
#[case("assets/index.html", "assets/**/*.html", true)]
#[case("assets/app.js", "assets/*.js", true)]
#[case("assets/app.js", "assets/**.js", true)]
#[case("assets/app.js", "assets/**/*.js", true)]
#[case("assets/index.html", "assets/*.{js,html}", true)]
#[case("assets/index.html", "assets/**.{js,html}", true)]
#[case("assets/index.html", "assets/**/*.{js,html}", true)]
#[case("assets/index.html", "assets/**/**.{js,html}", true)]
#[case("assets/app.js", "assets/*.{js,html}", true)]
#[case("assets/app.js", "assets/**.{js,html}", true)]
#[case("assets/app.js", "assets/**/*.{js,html}", true)]
#[case("assets/app.js", "assets/**/**.{js,html}", true)]
fn matches_asset_pattern(
#[case] asset_path: &str,
#[case] config_pattern: &str,
#[case] expected: bool,
) {
let asset = Asset::new(asset_path, vec![]);
let config: NormalizedAssetConfig = AssetConfig::Pattern {
pattern: config_pattern.to_string(),
content_type: None,
headers: vec![],
encodings: vec![],
}
.try_into()
.unwrap();
assert_eq!(config.matches_asset(&asset), expected);
}
#[rstest]
#[case("index.html")]
#[case("app.js")]
#[case("index.js")]
#[case("index.css")]
fn does_not_match_asset_redirect(#[case] asset_path: &str) {
let asset = Asset::new(asset_path, vec![]);
let config: NormalizedAssetConfig = AssetConfig::Redirect {
from: asset_path.to_string(),
to: asset_path.to_string(),
kind: AssetRedirectKind::Permanent,
headers: vec![(
"content-type".to_string(),
"text/plain; charset=utf-8".to_string(),
)],
}
.try_into()
.unwrap();
assert!(!config.matches_asset(&asset));
}
#[rstest]
fn asset_encoding_to_string() {
assert_eq!(AssetEncoding::Brotli.to_string(), "br");
assert_eq!(AssetEncoding::Zstd.to_string(), "zstd");
assert_eq!(AssetEncoding::Gzip.to_string(), "gzip");
assert_eq!(AssetEncoding::Deflate.to_string(), "deflate");
assert_eq!(AssetEncoding::Identity.to_string(), "identity");
}
}