use crate::resources::{MimeType, Resource, ResourceType};
use base64::{engine::Engine as _, prelude::BASE64_STANDARD};
use memchr::memmem;
use regex::Regex;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::sync::LazyLock;
static TOP_COMMENT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^/\*[\S\s]+?\n\*/\s*"#).unwrap());
static NON_EMPTY_LINE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\S"#).unwrap());
struct ResourceProperties {
name: String,
alias: Vec<String>,
#[allow(unused)]
data: Option<String>,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum ResourceAliasField {
SingleString(String),
ListOfStrings(Vec<String>),
}
impl ResourceAliasField {
fn into_vec(self) -> Vec<String> {
match self {
Self::SingleString(s) => vec![s],
Self::ListOfStrings(l) => l,
}
}
}
#[derive(serde::Deserialize)]
struct JsResourceProperties {
#[serde(default)]
alias: Option<ResourceAliasField>,
#[serde(default)]
data: Option<String>,
#[serde(default)]
params: Option<Vec<String>>,
}
type JsResourceEntry = (String, JsResourceProperties);
const REDIRECTABLE_RESOURCES_DECLARATION: &str = "export default new Map([";
static MAP_END_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"^\s*\]\s*\)"#).unwrap());
static TRAILING_COMMA_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#",([\],\}])"#).unwrap());
static UNQUOTED_FIELD_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"([\{,])([a-zA-Z][a-zA-Z0-9_]*):"#).unwrap());
static TRAILING_BLOCK_COMMENT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"\s*/\*[^'"]*\*/\s*$"#).unwrap());
fn read_redirectable_resource_mapping(mapfile_data: &str) -> Vec<ResourceProperties> {
let mut map: String = mapfile_data
.lines()
.skip_while(|line| *line != REDIRECTABLE_RESOURCES_DECLARATION)
.take_while(|line| !MAP_END_RE.is_match(line))
.map(|line| {
if let Some(i) = memmem::find(line.as_bytes(), b"//") {
&line[..i]
} else {
line
}
})
.map(|line| TRAILING_BLOCK_COMMENT_RE.replace_all(line, ""))
.fold(String::new(), |s, line| s + &line);
map.push(']');
assert!(map.starts_with(REDIRECTABLE_RESOURCES_DECLARATION));
map = map[REDIRECTABLE_RESOURCES_DECLARATION.len() - 1..].replace('\'', "\"");
map.retain(|c| !c.is_whitespace());
map = TRAILING_COMMA_RE
.replace_all(&map, |caps: ®ex::Captures| caps[1].to_string())
.to_string();
map = UNQUOTED_FIELD_RE
.replace_all(&map, |caps: ®ex::Captures| {
format!("{}\"{}\":", &caps[1], &caps[2])
})
.to_string();
let parsed: Vec<JsResourceEntry> = serde_json::from_str(&map).unwrap();
parsed
.into_iter()
.filter_map(|(name, props)| {
if props.params.is_some() {
None
} else {
Some(ResourceProperties {
name,
alias: props.alias.map(|a| a.into_vec()).unwrap_or_default(),
data: props.data,
})
}
})
.collect()
}
fn read_template_resources(scriptlets_data: &str) -> Vec<Resource> {
let mut resources = Vec::new();
let uncommented = TOP_COMMENT_RE.replace_all(scriptlets_data, "");
let mut name: Option<&str> = None;
let mut details = std::collections::HashMap::<_, Vec<_>>::new();
let mut script = String::new();
for line in uncommented.lines() {
if line.starts_with('#') || line.starts_with("// ") || line == "//" {
continue;
}
if name.is_none() {
if let Some(stripped) = line.strip_prefix("/// ") {
name = Some(stripped.trim());
}
continue;
}
if let Some(stripped) = line.strip_prefix("/// ") {
let mut line = stripped.split_whitespace();
let prop = line.next().expect("Detail line has property name");
let value = line.next().expect("Detail line has property value");
details
.entry(prop)
.and_modify(|v| v.push(value))
.or_insert_with(|| vec![value]);
continue;
}
if NON_EMPTY_LINE_RE.is_match(line) {
script += line.trim();
script.push('\n');
continue;
}
let kind = if script.contains("{{1}}") {
ResourceType::Template
} else {
ResourceType::Mime(MimeType::ApplicationJavascript)
};
resources.push(Resource {
name: name.expect("Resource name must be specified").to_owned(),
aliases: details
.get("alias")
.map(|aliases| aliases.iter().map(|alias| alias.to_string()).collect())
.unwrap_or_default(),
kind,
content: BASE64_STANDARD.encode(&script),
dependencies: vec![],
permission: Default::default(),
});
name = None;
details.clear();
script.clear();
}
resources
}
fn build_resource_from_file_contents(
resource_contents: &[u8],
resource_info: &ResourceProperties,
) -> Resource {
let name = resource_info.name.to_owned();
let aliases = resource_info
.alias
.iter()
.map(|alias| alias.to_string())
.collect();
let mimetype = MimeType::from_extension(&resource_info.name[..]);
let content = match mimetype {
MimeType::ApplicationJavascript | MimeType::TextHtml | MimeType::TextPlain => {
let utf8string = std::str::from_utf8(resource_contents).unwrap();
BASE64_STANDARD.encode(utf8string.replace('\r', ""))
}
_ => BASE64_STANDARD.encode(resource_contents),
};
Resource {
name,
aliases,
kind: ResourceType::Mime(mimetype),
content,
dependencies: vec![],
permission: Default::default(),
}
}
fn read_resource_from_web_accessible_dir(
web_accessible_resource_dir: &Path,
resource_info: &ResourceProperties,
) -> Resource {
let resource_path = web_accessible_resource_dir.join(&resource_info.name);
if !resource_path.is_file() {
panic!("Expected {resource_path:?} to be a file");
}
let mut resource_file = File::open(resource_path).expect("open resource file for reading");
let mut resource_contents = Vec::new();
resource_file
.read_to_end(&mut resource_contents)
.expect("read resource file contents");
build_resource_from_file_contents(&resource_contents, resource_info)
}
pub fn assemble_web_accessible_resources(
web_accessible_resource_dir: &Path,
redirect_resources_path: &Path,
) -> Vec<Resource> {
let mapfile_data = std::fs::read_to_string(redirect_resources_path).expect("read aliases path");
let resource_properties = read_redirectable_resource_mapping(&mapfile_data);
resource_properties
.iter()
.map(|resource_info| {
read_resource_from_web_accessible_dir(web_accessible_resource_dir, resource_info)
})
.collect()
}
#[deprecated]
pub fn assemble_scriptlet_resources(scriptlets_path: &Path) -> Vec<Resource> {
let scriptlets_data = std::fs::read_to_string(scriptlets_path).expect("read scriptlets path");
read_template_resources(&scriptlets_data)
}
#[cfg(test)]
#[path = "../../tests/unit/resources/resource_assembler.rs"]
mod unit_tests;