use base64::Engine;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use sha2::{Digest, Sha256};
use std::{
collections::HashMap,
fs::File,
path::{Path, PathBuf},
};
use tauri_utils::config::PatternKind;
use tauri_utils::{assets::AssetKey, config::DisabledCspModificationKind};
use thiserror::Error;
use walkdir::{DirEntry, WalkDir};
#[cfg(feature = "compression")]
use brotli::enc::backward_references::BrotliEncoderParams;
const TARGET_PATH: &str = "tauri-codegen-assets";
type Asset = (AssetKey, (PathBuf, PathBuf));
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EmbeddedAssetsError {
#[error("failed to read asset at {path} because {error}")]
AssetRead {
path: PathBuf,
error: std::io::Error,
},
#[error("failed to write asset from {path} to Vec<u8> because {error}")]
AssetWrite {
path: PathBuf,
error: std::io::Error,
},
#[error("failed to create hex from bytes because {0}")]
Hex(std::fmt::Error),
#[error("invalid prefix {prefix} used while including path {path}")]
PrefixInvalid { prefix: PathBuf, path: PathBuf },
#[error("invalid extension `{extension}` used for image {path}, must be `ico` or `png`")]
InvalidImageExtension { extension: PathBuf, path: PathBuf },
#[error("failed to walk directory {path} because {error}")]
Walkdir {
path: PathBuf,
error: walkdir::Error,
},
#[error("OUT_DIR env var is not set, do you have a build script?")]
OutDir,
#[error("version error: {0}")]
Version(#[from] semver::Error),
}
pub type EmbeddedAssetsResult<T> = Result<T, EmbeddedAssetsError>;
#[derive(Default)]
pub struct EmbeddedAssets {
assets: HashMap<AssetKey, (PathBuf, PathBuf)>,
csp_hashes: CspHashes,
}
pub struct EmbeddedAssetsInput(Vec<PathBuf>);
impl From<PathBuf> for EmbeddedAssetsInput {
fn from(path: PathBuf) -> Self {
Self(vec![path])
}
}
impl From<Vec<PathBuf>> for EmbeddedAssetsInput {
fn from(paths: Vec<PathBuf>) -> Self {
Self(paths)
}
}
struct RawEmbeddedAssets {
paths: Vec<(PathBuf, DirEntry)>,
csp_hashes: CspHashes,
}
impl RawEmbeddedAssets {
fn new(input: EmbeddedAssetsInput, options: &AssetOptions) -> Result<Self, EmbeddedAssetsError> {
let mut csp_hashes = CspHashes::default();
input
.0
.into_iter()
.flat_map(|path| {
let prefix = if path.is_dir() {
path.clone()
} else {
path
.parent()
.expect("embedded file asset has no parent")
.to_path_buf()
};
WalkDir::new(&path)
.follow_links(true)
.contents_first(true)
.into_iter()
.map(move |entry| (prefix.clone(), entry))
})
.filter_map(|(prefix, entry)| {
match entry {
Ok(entry) if entry.file_type().is_dir() => None,
Ok(entry) => {
if let Err(error) = csp_hashes
.add_if_applicable(&entry, &options.dangerous_disable_asset_csp_modification)
{
Some(Err(error))
} else {
Some(Ok((prefix, entry)))
}
}
Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
path: prefix,
error,
})),
}
})
.collect::<Result<Vec<(PathBuf, DirEntry)>, _>>()
.map(|paths| Self { paths, csp_hashes })
}
}
#[derive(Debug, Default)]
pub struct CspHashes {
pub(crate) scripts: Vec<String>,
pub(crate) inline_scripts: HashMap<String, Vec<String>>,
pub(crate) styles: Vec<String>,
}
impl CspHashes {
pub fn add_if_applicable(
&mut self,
entry: &DirEntry,
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
) -> Result<(), EmbeddedAssetsError> {
let path = entry.path();
if let Some("js") | Some("mjs") = path.extension().and_then(|os| os.to_str()) {
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
let mut hasher = Sha256::new();
hasher.update(
&std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
path: path.to_path_buf(),
error,
})?,
);
let hash = hasher.finalize();
self.scripts.push(format!(
"'sha256-{}'",
base64::engine::general_purpose::STANDARD.encode(hash)
));
}
}
Ok(())
}
}
#[derive(Default)]
pub struct AssetOptions {
pub(crate) csp: bool,
pub(crate) pattern: PatternKind,
pub(crate) freeze_prototype: bool,
pub(crate) dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
#[cfg(feature = "isolation")]
pub(crate) isolation_schema: String,
}
impl AssetOptions {
pub fn new(pattern: PatternKind) -> Self {
Self {
csp: false,
pattern,
freeze_prototype: false,
dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
#[cfg(feature = "isolation")]
isolation_schema: format!("isolation-{}", uuid::Uuid::new_v4()),
}
}
#[must_use]
pub fn with_csp(mut self) -> Self {
self.csp = true;
self
}
#[must_use]
pub fn freeze_prototype(mut self, freeze: bool) -> Self {
self.freeze_prototype = freeze;
self
}
pub fn dangerous_disable_asset_csp_modification(
mut self,
dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
) -> Self {
self.dangerous_disable_asset_csp_modification = dangerous_disable_asset_csp_modification;
self
}
}
impl EmbeddedAssets {
pub fn new(
input: impl Into<EmbeddedAssetsInput>,
options: &AssetOptions,
mut map: impl FnMut(
&AssetKey,
&Path,
&mut Vec<u8>,
&mut CspHashes,
) -> Result<(), EmbeddedAssetsError>,
) -> Result<Self, EmbeddedAssetsError> {
let RawEmbeddedAssets { paths, csp_hashes } = RawEmbeddedAssets::new(input.into(), options)?;
struct CompressState {
csp_hashes: CspHashes,
assets: HashMap<AssetKey, (PathBuf, PathBuf)>,
}
let CompressState { assets, csp_hashes } = paths.into_iter().try_fold(
CompressState {
csp_hashes,
assets: HashMap::new(),
},
move |mut state, (prefix, entry)| {
let (key, asset) =
Self::compress_file(&prefix, entry.path(), &mut map, &mut state.csp_hashes)?;
state.assets.insert(key, asset);
Result::<_, EmbeddedAssetsError>::Ok(state)
},
)?;
Ok(Self { assets, csp_hashes })
}
#[cfg(feature = "compression")]
fn compression_settings() -> BrotliEncoderParams {
let mut settings = BrotliEncoderParams::default();
if cfg!(debug_assertions) {
settings.quality = 2
} else {
settings.quality = 9
}
settings
}
fn compress_file(
prefix: &Path,
path: &Path,
map: &mut impl FnMut(
&AssetKey,
&Path,
&mut Vec<u8>,
&mut CspHashes,
) -> Result<(), EmbeddedAssetsError>,
csp_hashes: &mut CspHashes,
) -> Result<Asset, EmbeddedAssetsError> {
let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
path: path.to_owned(),
error,
})?;
let key = path
.strip_prefix(prefix)
.map(AssetKey::from) .map_err(|_| EmbeddedAssetsError::PrefixInvalid {
prefix: prefix.to_owned(),
path: path.to_owned(),
})?;
map(&key, path, &mut input, csp_hashes)?;
let out_dir = std::env::var("OUT_DIR")
.map_err(|_| EmbeddedAssetsError::OutDir)
.map(PathBuf::from)
.and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))
.map(|p| p.join(TARGET_PATH))?;
std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
let hash = crate::checksum(&input).map_err(EmbeddedAssetsError::Hex)?;
let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
out_dir.join(format!("{hash}.{ext}"))
} else {
out_dir.join(hash)
};
if !out_path.exists() {
#[allow(unused_mut)]
let mut out_file =
File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite {
path: out_path.clone(),
error,
})?;
#[cfg(not(feature = "compression"))]
{
use std::io::Write;
out_file
.write_all(&input)
.map_err(|error| EmbeddedAssetsError::AssetWrite {
path: path.to_owned(),
error,
})?;
}
#[cfg(feature = "compression")]
{
let mut input = std::io::Cursor::new(input);
brotli::BrotliCompress(&mut input, &mut out_file, &Self::compression_settings()).map_err(
|error| EmbeddedAssetsError::AssetWrite {
path: path.to_owned(),
error,
},
)?;
}
}
Ok((key, (path.into(), out_path)))
}
}
impl ToTokens for EmbeddedAssets {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut assets = TokenStream::new();
for (key, (input, output)) in &self.assets {
let key: &str = key.as_ref();
let input = input.display().to_string();
let output = output.display().to_string();
assets.append_all(quote!(#key => {
const _: &[u8] = include_bytes!(#input);
include_bytes!(#output)
},));
}
let mut global_hashes = TokenStream::new();
for script_hash in &self.csp_hashes.scripts {
let hash = script_hash.as_str();
global_hashes.append_all(quote!(CspHash::Script(#hash),));
}
for style_hash in &self.csp_hashes.styles {
let hash = style_hash.as_str();
global_hashes.append_all(quote!(CspHash::Style(#hash),));
}
let mut html_hashes = TokenStream::new();
for (path, hashes) in &self.csp_hashes.inline_scripts {
let key = path.as_str();
let mut value = TokenStream::new();
for script_hash in hashes {
let hash = script_hash.as_str();
value.append_all(quote!(CspHash::Script(#hash),));
}
html_hashes.append_all(quote!(#key => &[#value],));
}
tokens.append_all(quote! {{
#[allow(unused_imports)]
use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map};
EmbeddedAssets::new(phf_map! { #assets }, &[#global_hashes], phf_map! { #html_hashes })
}});
}
}
pub(crate) fn ensure_out_dir() -> EmbeddedAssetsResult<PathBuf> {
let out_dir = std::env::var("OUT_DIR")
.map_err(|_| EmbeddedAssetsError::OutDir)
.map(PathBuf::from)
.and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?;
std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
Ok(out_dir)
}