123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- 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;
- /// The subdirectory inside the target directory we want to place assets.
- const TARGET_PATH: &str = "tauri-codegen-assets";
- /// (key, (original filepath, compressed bytes))
- type Asset = (AssetKey, (PathBuf, PathBuf));
- /// All possible errors while reading and compressing an [`EmbeddedAssets`] directory
- #[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>;
- /// Represent a directory of assets that are compressed and embedded.
- ///
- /// This is the compile time generation of [`tauri_utils::assets::Assets`] from a directory. Assets
- /// from the directory are added as compiler dependencies by dummy including the original,
- /// uncompressed assets.
- ///
- /// The assets are compressed during this runtime, and can only be represented as a [`TokenStream`]
- /// through [`ToTokens`]. The generated code is meant to be injected into an application to include
- /// the compressed assets in that application's binary.
- #[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)
- }
- }
- /// Holds a list of (prefix, entry)
- struct RawEmbeddedAssets {
- paths: Vec<(PathBuf, DirEntry)>,
- csp_hashes: CspHashes,
- }
- impl RawEmbeddedAssets {
- /// Creates a new list of (prefix, entry) from a collection of inputs.
- 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 {
- // we only serve files, not directory listings
- Ok(entry) if entry.file_type().is_dir() => None,
- // compress all files encountered
- 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)))
- }
- }
- // pass down error through filter to fail when encountering any error
- Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
- path: prefix,
- error,
- })),
- }
- })
- .collect::<Result<Vec<(PathBuf, DirEntry)>, _>>()
- .map(|paths| Self { paths, csp_hashes })
- }
- }
- /// Holds all hashes that we will apply on the CSP tag/header.
- #[derive(Debug, Default)]
- pub struct CspHashes {
- /// Scripts that are part of the asset collection (JS or MJS files).
- pub(crate) scripts: Vec<String>,
- /// Inline scripts (`<script>code</script>`). Maps a HTML path to a list of hashes.
- pub(crate) inline_scripts: HashMap<String, Vec<String>>,
- /// A list of hashes of the contents of all `style` elements.
- pub(crate) styles: Vec<String>,
- }
- impl CspHashes {
- /// Only add a CSP hash to the appropriate category if we think the file matches
- ///
- /// Note: this only checks the file extension, much like how a browser will assume a .js file is
- /// a JavaScript file unless HTTP headers tell it otherwise.
- pub fn add_if_applicable(
- &mut self,
- entry: &DirEntry,
- dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
- ) -> Result<(), EmbeddedAssetsError> {
- let path = entry.path();
- // we only hash JavaScript files for now, may expand to other CSP hashable types in the future
- 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(())
- }
- }
- /// Options used to embed assets.
- #[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 {
- /// Creates the default asset options.
- 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()),
- }
- }
- /// Instruct the asset handler to inject the CSP token to HTML files (Linux only) and add asset nonces and hashes to the policy.
- #[must_use]
- pub fn with_csp(mut self) -> Self {
- self.csp = true;
- self
- }
- /// Instruct the asset handler to include a script to freeze the `Object.prototype` on all HTML files.
- #[must_use]
- pub fn freeze_prototype(mut self, freeze: bool) -> Self {
- self.freeze_prototype = freeze;
- self
- }
- /// Instruct the asset handler to **NOT** modify the CSP. This is **NOT** recommended.
- 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 {
- /// Compress a collection of files and directories, ready to be generated into [`Assets`].
- ///
- /// [`Assets`]: tauri_utils::assets::Assets
- 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> {
- // we need to pre-compute all files now, so that we can inject data from all files into a few
- 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 })
- }
- /// Use highest compression level for release, the fastest one for everything else
- #[cfg(feature = "compression")]
- fn compression_settings() -> BrotliEncoderParams {
- let mut settings = BrotliEncoderParams::default();
- // the following compression levels are hand-picked and are not min-maxed.
- // they have a good balance of runtime vs size for the respective profile goals.
- // see the "brotli" section of this comment https://github.com/tauri-apps/tauri/issues/3571#issuecomment-1054847558
- if cfg!(debug_assertions) {
- settings.quality = 2
- } else {
- settings.quality = 9
- }
- settings
- }
- /// Compress a file and spit out the information in a [`HashMap`] friendly form.
- 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,
- })?;
- // get a key to the asset path without the asset directory prefix
- let key = path
- .strip_prefix(prefix)
- .map(AssetKey::from) // format the path for use in assets
- .map_err(|_| EmbeddedAssetsError::PrefixInvalid {
- prefix: prefix.to_owned(),
- path: path.to_owned(),
- })?;
- // perform any caller-requested input manipulation
- map(&key, path, &mut input, csp_hashes)?;
- // we must canonicalize the base of our paths to allow long paths on windows
- 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))?;
- // make sure that our output directory is created
- std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
- // get a hash of the input - allows for caching existing files
- let hash = crate::checksum(&input).map_err(EmbeddedAssetsError::Hex)?;
- // use the content hash to determine filename, keep extensions that exist
- 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)
- };
- // only compress and write to the file if it doesn't already exist.
- 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);
- // entirely write input to the output file path with compression
- 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();
- // add original asset as a compiler dependency, rely on dead code elimination to clean it up
- 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],));
- }
- // we expect phf related items to be in path when generating the path code
- 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))?;
- // make sure that our output directory is created
- std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
- Ok(out_dir)
- }
|