snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 17:00:42 +05:00
commit feda81bf2f
1178 changed files with 156404 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::collections::BTreeMap;
use std::convert::identity;
use std::path::{Path, PathBuf};
use std::{ffi::OsStr, str::FromStr};
use crate::{
embedded_assets::{
AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult, ensure_out_dir,
},
image::CachedIcon,
};
use base64::Engine;
use proc_macro2::TokenStream;
use quote::quote;
use sha2::{Digest, Sha256};
use syn::Expr;
use tauri_utils::{
acl::{
ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME, get_capabilities, manifest::Manifest,
resolved::Resolved,
},
assets::AssetKey,
config::{Config, FrontendDist, PatternKind},
html2::{Document, inject_nonce_token, parse_doc, serialize_doc},
platform::Target,
tokens::{map_lit, str_lit},
};
/// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
pub struct ContextData {
pub dev: bool,
pub config: Config,
pub config_parent: PathBuf,
pub root: TokenStream,
/// Additional capabilities to include.
pub capabilities: Option<Vec<PathBuf>>,
/// The custom assets implementation
pub assets: Option<Expr>,
/// Skip runtime-only types generation for tests (e.g. embed-plist usage).
pub test: bool,
}
fn inject_script_hashes(document: &Document, key: &AssetKey, csp_hashes: &mut CspHashes) {
let script_elements = document.select("script:not(:empty)");
let scripts = script_elements
.iter()
.map(|element| {
let script = tauri_utils::html2::normalize_script_for_csp(element.text().as_bytes());
let script_hash = Sha256::digest(script);
let hash_base64 = base64::engine::general_purpose::STANDARD.encode(script_hash);
format!("'sha256-{hash_base64}'")
})
.collect::<Vec<_>>();
csp_hashes
.inline_scripts
.entry(key.clone().into())
.or_default()
.extend(scripts);
}
fn map_core_assets(
options: &AssetOptions,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> + use<> {
let csp = options.csp;
let dangerous_disable_asset_csp_modification =
options.dangerous_disable_asset_csp_modification.clone();
move |key, path, input, csp_hashes| {
if path.extension() == Some(OsStr::new("html")) {
#[allow(clippy::collapsible_if)]
if csp {
let document = parse_doc(String::from_utf8_lossy(input).into_owned());
inject_nonce_token(&document, &dangerous_disable_asset_csp_modification);
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
inject_script_hashes(&document, key, csp_hashes);
}
*input = serialize_doc(&document);
}
}
Ok(())
}
}
#[cfg(feature = "isolation")]
fn map_isolation(
_options: &AssetOptions,
dir: PathBuf,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> + use<> {
// create the csp for the isolation iframe styling now, to make the runtime less complex
let mut hasher = Sha256::new();
hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
let hash = hasher.finalize();
let iframe_style_csp_hash = format!(
"'sha256-{}'",
base64::engine::general_purpose::STANDARD.encode(hash)
);
move |key, path, input, csp_hashes| {
if path.extension() == Some(OsStr::new("html")) {
let isolation_html = parse_doc(String::from_utf8_lossy(input).into_owned());
// this is appended, so no need to reverse order it
tauri_utils::html2::inject_codegen_isolation_script(&isolation_html);
// temporary workaround for windows not loading assets
tauri_utils::html2::inline_isolation(&isolation_html, &dir);
inject_nonce_token(
&isolation_html,
&tauri_utils::config::DisabledCspModificationKind::Flag(false),
);
inject_script_hashes(&isolation_html, key, csp_hashes);
csp_hashes.styles.push(iframe_style_csp_hash.clone());
*input = serialize_doc(&isolation_html)
}
Ok(())
}
}
/// Build a `tauri::Context` for including in application code.
pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
let ContextData {
dev,
config,
config_parent,
root,
capabilities: additional_capabilities,
assets,
test,
} = data;
#[allow(unused_variables)]
let running_tests = test;
let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
.as_deref()
.map(Target::from_triple)
.unwrap_or_else(|_| Target::current());
let mut options = AssetOptions::new(config.app.security.pattern.clone())
.freeze_prototype(config.app.security.freeze_prototype)
.dangerous_disable_asset_csp_modification(
config
.app
.security
.dangerous_disable_asset_csp_modification
.clone(),
);
let csp = if dev {
config
.app
.security
.dev_csp
.as_ref()
.or(config.app.security.csp.as_ref())
} else {
config.app.security.csp.as_ref()
};
if csp.is_some() {
options = options.with_csp();
}
let assets = if let Some(assets) = assets {
quote!(#assets)
} else if dev && config.build.dev_url.is_some() {
let assets = EmbeddedAssets::default();
quote!(#assets)
} else {
let assets = match &config.build.frontend_dist {
Some(url) => match url {
FrontendDist::Url(_url) => Default::default(),
FrontendDist::Directory(path) => {
let assets_path = config_parent.join(path);
if !assets_path.exists() {
panic!(
"The `frontendDist` configuration is set to `{path:?}` but this path doesn't exist"
)
}
EmbeddedAssets::new(assets_path, &options, map_core_assets(&options))?
}
FrontendDist::Files(files) => EmbeddedAssets::new(
files
.iter()
.map(|p| config_parent.join(p))
.collect::<Vec<_>>(),
&options,
map_core_assets(&options),
)?,
_ => unimplemented!(),
},
None => Default::default(),
};
quote!(#assets)
};
let out_dir = ensure_out_dir()?;
let default_window_icon = {
if target == Target::Windows {
// handle default window icons for Windows targets
quote!(#root::image::default_window_icon_from_app_icon_resource())
} else {
// handle default window icons for Unix targets
let icon_path = find_icon(
&config,
&config_parent,
|i| i.ends_with(".png"),
"icons/icon.png",
);
let icon = CachedIcon::new(&root, &icon_path)?;
quote!(::std::option::Option::Some(#icon))
}
};
let app_icon = if target == Target::MacOS && dev {
let mut icon_path = find_icon(
&config,
&config_parent,
|i| i.ends_with(".icns"),
"icons/icon.png",
);
if !icon_path.exists() {
icon_path = find_icon(
&config,
&config_parent,
|i| i.ends_with(".png"),
"icons/icon.png",
);
}
let icon = CachedIcon::new_raw(&root, &icon_path)?;
quote!(::std::option::Option::Some(#icon.to_vec()))
} else {
quote!(::std::option::Option::None)
};
let package_name = if let Some(product_name) = &config.product_name {
quote!(#product_name.to_string())
} else {
quote!(env!("CARGO_PKG_NAME").to_string())
};
let package_version = if let Some(version) = &config.version {
semver::Version::from_str(version)?;
quote!(#version.to_string())
} else {
quote!(env!("CARGO_PKG_VERSION").to_string())
};
let package_info = quote!(
#root::PackageInfo {
name: #package_name,
version: #package_version.parse().unwrap(),
authors: env!("CARGO_PKG_AUTHORS"),
description: env!("CARGO_PKG_DESCRIPTION"),
crate_name: env!("CARGO_PKG_NAME"),
}
);
let with_tray_icon_code = if target.is_desktop() {
if let Some(tray) = &config.app.tray_icon {
let tray_icon_icon_path = config_parent.join(&tray.icon_path);
let icon = CachedIcon::new(&root, &tray_icon_icon_path)?;
quote!(context.set_tray_icon(::std::option::Option::Some(#icon));)
} else {
quote!()
}
} else {
quote!()
};
#[cfg(target_os = "macos")]
let maybe_embed_plist_block = if target == Target::MacOS && dev && !running_tests {
let info_plist_path = config_parent.join("Info.plist");
let mut info_plist = if info_plist_path.exists() {
plist::Value::from_file(&info_plist_path)
.unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
} else {
plist::Value::Dictionary(Default::default())
};
if let Some(plist) = info_plist.as_dictionary_mut() {
if let Some(bundle_name) = config
.bundle
.macos
.bundle_name
.as_ref()
.or(config.product_name.as_ref())
{
plist.insert("CFBundleName".into(), bundle_name.as_str().into());
}
if let Some(version) = &config.version {
let bundle_version = &config.bundle.macos.bundle_version;
plist.insert("CFBundleShortVersionString".into(), version.clone().into());
plist.insert(
"CFBundleVersion".into(),
bundle_version
.clone()
.unwrap_or_else(|| version.clone())
.into(),
);
}
}
let mut plist_contents = std::io::BufWriter::new(Vec::new());
info_plist
.to_writer_xml(&mut plist_contents)
.expect("failed to serialize plist");
let plist_contents =
String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
let plist = crate::Cached::try_from(plist_contents)?;
quote!({
tauri::embed_plist::embed_info_plist!(#plist);
})
} else {
quote!()
};
#[cfg(not(target_os = "macos"))]
let maybe_embed_plist_block = quote!();
let pattern = match &options.pattern {
PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
#[cfg(not(feature = "isolation"))]
PatternKind::Isolation { dir: _ } => {
quote!(#root::Pattern::Brownfield)
}
#[cfg(feature = "isolation")]
PatternKind::Isolation { dir } => {
let dir = config_parent.join(dir);
if !dir.exists() {
panic!("The isolation application path is set to `{dir:?}` but it does not exist")
}
let mut sets_isolation_hook = false;
let key = uuid::Uuid::new_v4().to_string();
let map_isolation = map_isolation(&options, dir.clone());
let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
// we check if `__TAURI_ISOLATION_HOOK__` exists in the isolation code
// before modifying the files since we inject our own `__TAURI_ISOLATION_HOOK__` reference in HTML files
if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
sets_isolation_hook = true;
}
map_isolation(key, path, input, csp_hashes)
})?;
if !sets_isolation_hook {
panic!(
"The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value."
);
}
let schema = options.isolation_schema;
quote!(#root::Pattern::Isolation {
assets: ::std::sync::Arc::new(#assets),
schema: #schema.into(),
key: #key.into(),
crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
})
}
};
let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
let acl_file =
std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
} else {
Default::default()
};
let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
let capabilities_from_files = if capabilities_file_path.exists() {
let capabilities_json =
std::fs::read_to_string(&capabilities_file_path).expect("failed to read capabilities");
serde_json::from_str(&capabilities_json).expect("failed to parse capabilities")
} else {
Default::default()
};
let capabilities = get_capabilities(
&config,
capabilities_from_files,
additional_capabilities.as_deref(),
)
.unwrap();
let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
let acl_tokens = map_lit(
quote! { ::std::collections::BTreeMap },
&acl,
str_lit,
identity,
);
let runtime_authority = quote!(#root::runtime_authority!(#acl_tokens, #resolved));
let plugin_global_api_scripts = if config.app.with_global_tauri {
if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
let scripts = scripts.into_iter().map(|s| quote!(#s));
quote!(::std::option::Option::Some(&[#(#scripts),*]))
} else {
quote!(::std::option::Option::None)
}
} else {
quote!(::std::option::Option::None)
};
let maybe_config_parent_setter = if dev {
let config_parent = config_parent.to_string_lossy();
quote!({
context.with_config_parent(#config_parent);
})
} else {
quote!()
};
let context = quote!({
#maybe_embed_plist_block
#[allow(unused_mut, clippy::let_and_return)]
let mut context = #root::Context::new(
#config,
::std::boxed::Box::new(assets),
#default_window_icon,
#app_icon,
#package_info,
#pattern,
#runtime_authority,
#plugin_global_api_scripts
);
#with_tray_icon_code
#maybe_config_parent_setter
context
});
// Wrapping in a function to make rust analyzer faster,
// see https://github.com/tauri-apps/tauri/pull/14457
// We take the assets as an argument so when the caller provides custom `assets` the closure
// does not capture from the caller's scope ("can't capture dynamic environment in a fn item").
let output = quote!({
fn inner<R: #root::Runtime, A: #root::Assets<R> + 'static>(assets: A) -> #root::Context<R> {
let thread = ::std::thread::Builder::new()
.name(String::from("generated tauri context creation"))
.stack_size(8 * 1024 * 1024)
.spawn(move || #context)
.expect("unable to create thread with 8MiB stack");
match thread.join() {
Ok(context) => context,
Err(_) => {
eprintln!("the generated Tauri `Context` panicked during creation");
::std::process::exit(101);
}
}
}
inner(#assets)
});
Ok(output)
}
fn find_icon(
config: &Config,
config_parent: &Path,
predicate: impl Fn(&&String) -> bool,
default: &str,
) -> PathBuf {
let icon_path = config
.bundle
.icon
.iter()
.find(predicate)
.map(AsRef::as_ref)
.unwrap_or(default);
config_parent.join(icon_path)
}

View File

@@ -0,0 +1,452 @@
// 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::{ToTokens, TokenStreamExt, quote};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
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: BTreeMap<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: BTreeMap<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(|b| tauri_utils::html2::normalize_script_for_csp(&b))
.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: BTreeMap<AssetKey, (PathBuf, PathBuf)>,
}
let CompressState { assets, csp_hashes } = paths.into_iter().try_fold(
CompressState {
csp_hashes,
assets: BTreeMap::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 [`BTreeMap`] 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();
// Sort the hashes so the generated code does not depend on the filesystem
// walk order the assets were collected in, which varies across machines
let mut script_hashes: Vec<_> = self.csp_hashes.scripts.iter().collect();
script_hashes.sort();
for script_hash in script_hashes {
let hash = script_hash.as_str();
global_hashes.append_all(quote!(CspHash::Script(#hash),));
}
let mut style_hashes: Vec<_> = self.csp_hashes.styles.iter().collect();
style_hashes.sort();
for style_hash in style_hashes {
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)
}

View File

@@ -0,0 +1,208 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
Cached,
embedded_assets::{EmbeddedAssetsError, EmbeddedAssetsResult},
};
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use std::{ffi::OsStr, io::Cursor, path::Path};
/// The format the Icon is consumed as.
pub(crate) enum IconFormat {
/// The image, completely unmodified.
Raw,
/// RGBA raw data, meant to be consumed by [`tauri::image::Image`].
Image { width: u32, height: u32 },
}
pub struct CachedIcon {
cache: Cached,
format: IconFormat,
root: TokenStream,
}
impl CachedIcon {
pub fn new(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
match icon.extension().map(OsStr::to_string_lossy).as_deref() {
Some("png") => Self::new_png(root, icon),
Some("ico") => Self::new_ico(root, icon),
unknown => Err(EmbeddedAssetsError::InvalidImageExtension {
extension: unknown.unwrap_or_default().into(),
path: icon.to_path_buf(),
}),
}
}
/// Cache the icon without any manipulation.
pub fn new_raw(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
let buf = Self::open(icon);
Cached::try_from(buf).map(|cache| Self {
cache,
root: root.clone(),
format: IconFormat::Raw,
})
}
/// Cache an ICO icon as RGBA data, see [`ImageFormat::Image`].
pub fn new_ico(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
let buf = Self::open(icon);
let icon_dir = ico::IconDir::read(Cursor::new(&buf))
.unwrap_or_else(|e| panic!("failed to parse icon {}: {}", icon.display(), e));
let entry = largest_ico_entry(&icon_dir)
.unwrap_or_else(|| panic!("icon {} has no entries", icon.display()));
let rgba = entry
.decode()
.unwrap_or_else(|e| panic!("failed to decode icon {}: {}", icon.display(), e))
.rgba_data()
.to_vec();
Cached::try_from(rgba).map(|cache| Self {
cache,
root: root.clone(),
format: IconFormat::Image {
width: entry.width(),
height: entry.height(),
},
})
}
/// Cache a PNG icon as RGBA data, see [`ImageFormat::Image`].
pub fn new_png(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
let buf = Self::open(icon);
let decoder = png::Decoder::new(Cursor::new(&buf));
let mut reader = decoder
.read_info()
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", icon.display(), e));
if reader.output_color_type().0 != png::ColorType::Rgba {
panic!("icon {} is not RGBA", icon.display());
}
let mut rgba = Vec::with_capacity(reader.output_buffer_size().unwrap());
while let Ok(Some(row)) = reader.next_row() {
rgba.extend(row.data());
}
Cached::try_from(rgba).map(|cache| Self {
cache,
root: root.clone(),
format: IconFormat::Image {
width: reader.info().width,
height: reader.info().height,
},
})
}
fn open(path: &Path) -> Vec<u8> {
std::fs::read(path).unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e))
}
}
/// Picks the entry of an ICO file to embed: the largest one (and, for equal sizes, the deepest).
///
/// ICO files conventionally store entries smallest-first, so taking the first entry would give
/// the OS a 16x16 image to upscale; the largest entry lets it downscale a high-resolution source instead.
fn largest_ico_entry(icon_dir: &ico::IconDir) -> Option<&ico::IconDirEntry> {
icon_dir
.entries()
.iter()
.max_by_key(|e| (e.width() * e.height(), e.bits_per_pixel()))
}
impl ToTokens for CachedIcon {
fn to_tokens(&self, tokens: &mut TokenStream) {
let root = &self.root;
let cache = &self.cache;
let raw = quote!(::std::include_bytes!(#cache));
tokens.append_all(match self.format {
IconFormat::Raw => raw,
IconFormat::Image { width, height } => {
quote!(#root::image::Image::new(#raw, #width, #height))
}
})
}
}
#[cfg(test)]
mod tests {
use super::largest_ico_entry;
use ico::{IconDir, IconDirEntry, IconImage, ResourceType};
use std::io::Cursor;
fn image(size: u32, alpha: u8) -> IconImage {
let rgba = [255, 0, 0, alpha].repeat((size * size) as usize);
IconImage::from_rgba_data(size, size, rgba)
}
/// Builds an ICO with an opaque entry for each size, in the given order, and reads it back
/// through the same parser `CachedIcon::new_ico` uses.
fn icon_dir(sizes: &[u32]) -> IconDir {
let mut dir = IconDir::new(ResourceType::Icon);
for &size in sizes {
let entry = if size == 256 {
IconDirEntry::encode_as_png(&image(size, 255)).unwrap()
} else {
IconDirEntry::encode_as_bmp(&image(size, 255)).unwrap()
};
dir.add_entry(entry);
}
let mut buf = Vec::new();
dir.write(&mut buf).unwrap();
IconDir::read(Cursor::new(buf)).unwrap()
}
#[test]
fn picks_largest_entry_regardless_of_order() {
// the entry order `tauri icon` used to generate, with the PNG-compressed 256 entry last
let dir = icon_dir(&[32, 16, 24, 48, 64, 256]);
let entry = largest_ico_entry(&dir).unwrap();
assert_eq!((entry.width(), entry.height()), (256, 256));
assert!(entry.is_png());
// largest first, in the middle and alone
for sizes in [&[64, 48, 16][..], &[16, 64, 48], &[64]] {
let dir = icon_dir(sizes);
let entry = largest_ico_entry(&dir).unwrap();
assert_eq!((entry.width(), entry.height()), (64, 64), "{sizes:?}");
}
}
#[test]
fn picks_deepest_entry_for_equal_sizes() {
// a single opaque color encodes as a low-depth BMP, a translucent one needs 32bpp
let shallow = IconDirEntry::encode_as_bmp(&image(32, 255)).unwrap();
let deep = IconDirEntry::encode_as_bmp(&image(32, 128)).unwrap();
assert!(shallow.bits_per_pixel() < deep.bits_per_pixel());
let deep_bpp = deep.bits_per_pixel();
for entries in [[shallow.clone(), deep.clone()], [deep, shallow]] {
let mut dir = IconDir::new(ResourceType::Icon);
for entry in entries {
dir.add_entry(entry);
}
let entry = largest_ico_entry(&dir).unwrap();
assert_eq!((entry.width(), entry.height()), (32, 32));
assert_eq!(entry.bits_per_pixel(), deep_bpp);
}
}
#[test]
fn selected_entry_decodes_at_its_own_size() {
let dir = icon_dir(&[16, 256]);
let entry = largest_ico_entry(&dir).unwrap();
let decoded = entry.decode().unwrap();
assert_eq!((decoded.width(), decoded.height()), (256, 256));
assert_eq!(decoded.rgba_data().len(), 256 * 256 * 4);
}
#[test]
fn no_entries() {
assert!(largest_ico_entry(&IconDir::new(ResourceType::Icon)).is_none());
}
}

View File

@@ -0,0 +1,152 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! - Embed, hash, and compress assets, including icons for the app as well as the tray icon.
//! - Parse `tauri.conf.json` at compile time and generate the Config struct.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
)]
pub use self::context::{ContextData, context_codegen};
use crate::embedded_assets::{EmbeddedAssetsError, ensure_out_dir};
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use std::{
borrow::Cow,
fmt::{self, Write},
path::{Path, PathBuf},
};
pub use tauri_utils::config::{Config, parse::ConfigError};
use tauri_utils::platform::Target;
use tauri_utils::write_if_changed;
mod context;
pub mod embedded_assets;
pub mod image;
#[doc(hidden)]
pub mod vendor;
/// Represents all the errors that can happen while reading the config during codegen.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CodegenConfigError {
#[error("unable to access current working directory: {0}")]
CurrentDir(std::io::Error),
// this error should be "impossible" because we use std::env::current_dir() - cover it anyways
#[error(
"Tauri config file has no parent, this shouldn't be possible. file an issue on https://github.com/tauri-apps/tauri - target {0}"
)]
Parent(PathBuf),
#[error("unable to parse inline JSON TAURI_CONFIG env var: {0}")]
FormatInline(serde_json::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("{0}")]
ConfigError(#[from] ConfigError),
}
/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path.
///
/// If the passed path is relative, it should be relative to the current working directory of the
/// compiling crate.
pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError> {
let path = if path.is_relative() {
let cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
Cow::Owned(cwd.join(path))
} else {
Cow::Borrowed(path)
};
// this should be impossible because of the use of `current_dir()` above, but handle it anyways
let parent = path
.parent()
.map(ToOwned::to_owned)
.ok_or_else(|| CodegenConfigError::Parent(path.into_owned()))?;
let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
.as_deref()
.map(Target::from_triple)
.unwrap_or_else(|_| Target::current());
// in the future we may want to find a way to not need the TAURI_CONFIG env var so that
// it is impossible for the content of two separate configs to get mixed up. The chances are
// already unlikely unless the developer goes out of their way to run the cli on a different
// project than the target crate.
let mut config =
serde_json::from_value(tauri_utils::config::parse::read_from(target, &parent)?.0)?;
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value =
serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?;
json_patch::merge(&mut config, &merge_config);
}
// Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly.
let old_cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
std::env::set_current_dir(parent.clone()).map_err(CodegenConfigError::CurrentDir)?;
let config = serde_json::from_value(config)?;
// Reset working directory.
std::env::set_current_dir(old_cwd).map_err(CodegenConfigError::CurrentDir)?;
Ok((config, parent))
}
/// Create a blake3 checksum of the passed bytes.
fn checksum(bytes: &[u8]) -> Result<String, fmt::Error> {
let mut hasher = vendor::blake3_reference::Hasher::default();
hasher.update(bytes);
let mut bytes = [0u8; 32];
hasher.finalize(&mut bytes);
let mut hex = String::with_capacity(2 * bytes.len());
for b in bytes {
write!(hex, "{b:02x}")?;
}
Ok(hex)
}
/// Cache the data to `$OUT_DIR`, only if it does not already exist.
///
/// Due to using a checksum as the filename, an existing file should be the exact same content
/// as the data being checked.
struct Cached {
checksum: String,
}
impl TryFrom<String> for Cached {
type Error = EmbeddedAssetsError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(Vec::from(value))
}
}
impl TryFrom<Vec<u8>> for Cached {
type Error = EmbeddedAssetsError;
fn try_from(content: Vec<u8>) -> Result<Self, Self::Error> {
let checksum = checksum(content.as_ref()).map_err(EmbeddedAssetsError::Hex)?;
let path = ensure_out_dir()?.join(&checksum);
write_if_changed(&path, &content)
.map(|_| Self { checksum })
.map_err(|error| EmbeddedAssetsError::AssetWrite { path, error })
}
}
impl ToTokens for Cached {
fn to_tokens(&self, tokens: &mut TokenStream) {
let path = &self.checksum;
tokens.append_all(quote!(::std::concat!(::std::env!("OUT_DIR"), "/", #path)))
}
}

View File

@@ -0,0 +1,378 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! This is a lightly modified version of the BLAKE3 reference implementation.
//!
//! The changes applied are to remove unused item warnings due to using it
//! vendored along with some minor clippy suggestions. No logic changes. I
//! suggest diffing against the original to find all the changes.
//!
//! ## Original Header
//! This is the reference implementation of BLAKE3. It is used for testing and
//! as a readable example of the algorithms involved. Section 5.1 of [the BLAKE3
//! spec](https://github.com/BLAKE3-team/BLAKE3-specs/blob/master/blake3.pdf)
//! discusses this implementation. You can render docs for this implementation
//! by running `cargo doc --open` in this directory.
//!
//! # Example
//!
//! ```
//! let mut hasher = tauri_codegen::vendor::blake3_reference::Hasher::new();
//! hasher.update(b"abc");
//! hasher.update(b"def");
//! let mut hash = [0; 32];
//! hasher.finalize(&mut hash);
//! let mut extended_hash = [0; 500];
//! hasher.finalize(&mut extended_hash);
//! assert_eq!(hash, extended_hash[..32]);
//! ```
//!
//! CC0-1.0 OR Apache-2.0
use core::cmp::min;
use core::convert::TryInto;
const OUT_LEN: usize = 32;
const BLOCK_LEN: usize = 64;
const CHUNK_LEN: usize = 1024;
const CHUNK_START: u32 = 1 << 0;
const CHUNK_END: u32 = 1 << 1;
const PARENT: u32 = 1 << 2;
const ROOT: u32 = 1 << 3;
const IV: [u32; 8] = [
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
];
const MSG_PERMUTATION: [usize; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
// The mixing function, G, which mixes either a column or a diagonal.
fn g(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, mx: u32, my: u32) {
state[a] = state[a].wrapping_add(state[b]).wrapping_add(mx);
state[d] = (state[d] ^ state[a]).rotate_right(16);
state[c] = state[c].wrapping_add(state[d]);
state[b] = (state[b] ^ state[c]).rotate_right(12);
state[a] = state[a].wrapping_add(state[b]).wrapping_add(my);
state[d] = (state[d] ^ state[a]).rotate_right(8);
state[c] = state[c].wrapping_add(state[d]);
state[b] = (state[b] ^ state[c]).rotate_right(7);
}
fn round(state: &mut [u32; 16], m: &[u32; 16]) {
// Mix the columns.
g(state, 0, 4, 8, 12, m[0], m[1]);
g(state, 1, 5, 9, 13, m[2], m[3]);
g(state, 2, 6, 10, 14, m[4], m[5]);
g(state, 3, 7, 11, 15, m[6], m[7]);
// Mix the diagonals.
g(state, 0, 5, 10, 15, m[8], m[9]);
g(state, 1, 6, 11, 12, m[10], m[11]);
g(state, 2, 7, 8, 13, m[12], m[13]);
g(state, 3, 4, 9, 14, m[14], m[15]);
}
fn permute(m: &mut [u32; 16]) {
let mut permuted = [0; 16];
for i in 0..16 {
permuted[i] = m[MSG_PERMUTATION[i]];
}
*m = permuted;
}
fn compress(
chaining_value: &[u32; 8],
block_words: &[u32; 16],
counter: u64,
block_len: u32,
flags: u32,
) -> [u32; 16] {
let mut state = [
chaining_value[0],
chaining_value[1],
chaining_value[2],
chaining_value[3],
chaining_value[4],
chaining_value[5],
chaining_value[6],
chaining_value[7],
IV[0],
IV[1],
IV[2],
IV[3],
counter as u32,
(counter >> 32) as u32,
block_len,
flags,
];
let mut block = *block_words;
round(&mut state, &block); // round 1
permute(&mut block);
round(&mut state, &block); // round 2
permute(&mut block);
round(&mut state, &block); // round 3
permute(&mut block);
round(&mut state, &block); // round 4
permute(&mut block);
round(&mut state, &block); // round 5
permute(&mut block);
round(&mut state, &block); // round 6
permute(&mut block);
round(&mut state, &block); // round 7
for i in 0..8 {
state[i] ^= state[i + 8];
state[i + 8] ^= chaining_value[i];
}
state
}
fn first_8_words(compression_output: [u32; 16]) -> [u32; 8] {
compression_output[0..8].try_into().unwrap()
}
fn words_from_little_endian_bytes(bytes: &[u8], words: &mut [u32]) {
debug_assert_eq!(bytes.len(), 4 * words.len());
for (four_bytes, word) in bytes.as_chunks::<4>().0.iter().zip(words) {
*word = u32::from_le_bytes(*four_bytes);
}
}
// Each chunk or parent node can produce either an 8-word chaining value or, by
// setting the ROOT flag, any number of final output bytes. The Output struct
// captures the state just prior to choosing between those two possibilities.
struct Output {
input_chaining_value: [u32; 8],
block_words: [u32; 16],
counter: u64,
block_len: u32,
flags: u32,
}
impl Output {
fn chaining_value(&self) -> [u32; 8] {
first_8_words(compress(
&self.input_chaining_value,
&self.block_words,
self.counter,
self.block_len,
self.flags,
))
}
fn root_output_bytes(&self, out_slice: &mut [u8]) {
for (output_block_counter, out_block) in (0u64..).zip(out_slice.chunks_mut(2 * OUT_LEN)) {
let words = compress(
&self.input_chaining_value,
&self.block_words,
output_block_counter,
self.block_len,
self.flags | ROOT,
);
// The output length might not be a multiple of 4.
for (word, out_word) in words.iter().zip(out_block.chunks_mut(4)) {
out_word.copy_from_slice(&word.to_le_bytes()[..out_word.len()]);
}
}
}
}
struct ChunkState {
chaining_value: [u32; 8],
chunk_counter: u64,
block: [u8; BLOCK_LEN],
block_len: u8,
blocks_compressed: u8,
flags: u32,
}
impl ChunkState {
fn new(key_words: [u32; 8], chunk_counter: u64, flags: u32) -> Self {
Self {
chaining_value: key_words,
chunk_counter,
block: [0; BLOCK_LEN],
block_len: 0,
blocks_compressed: 0,
flags,
}
}
fn len(&self) -> usize {
BLOCK_LEN * self.blocks_compressed as usize + self.block_len as usize
}
fn start_flag(&self) -> u32 {
if self.blocks_compressed == 0 {
CHUNK_START
} else {
0
}
}
fn update(&mut self, mut input: &[u8]) {
while !input.is_empty() {
// If the block buffer is full, compress it and clear it. More
// input is coming, so this compression is not CHUNK_END.
if self.block_len as usize == BLOCK_LEN {
let mut block_words = [0; 16];
words_from_little_endian_bytes(&self.block, &mut block_words);
self.chaining_value = first_8_words(compress(
&self.chaining_value,
&block_words,
self.chunk_counter,
BLOCK_LEN as u32,
self.flags | self.start_flag(),
));
self.blocks_compressed += 1;
self.block = [0; BLOCK_LEN];
self.block_len = 0;
}
// Copy input bytes into the block buffer.
let want = BLOCK_LEN - self.block_len as usize;
let take = min(want, input.len());
self.block[self.block_len as usize..][..take].copy_from_slice(&input[..take]);
self.block_len += take as u8;
input = &input[take..];
}
}
fn output(&self) -> Output {
let mut block_words = [0; 16];
words_from_little_endian_bytes(&self.block, &mut block_words);
Output {
input_chaining_value: self.chaining_value,
block_words,
counter: self.chunk_counter,
block_len: self.block_len as u32,
flags: self.flags | self.start_flag() | CHUNK_END,
}
}
}
fn parent_output(
left_child_cv: [u32; 8],
right_child_cv: [u32; 8],
key_words: [u32; 8],
flags: u32,
) -> Output {
let mut block_words = [0; 16];
block_words[..8].copy_from_slice(&left_child_cv);
block_words[8..].copy_from_slice(&right_child_cv);
Output {
input_chaining_value: key_words,
block_words,
counter: 0, // Always 0 for parent nodes.
block_len: BLOCK_LEN as u32, // Always BLOCK_LEN (64) for parent nodes.
flags: PARENT | flags,
}
}
fn parent_cv(
left_child_cv: [u32; 8],
right_child_cv: [u32; 8],
key_words: [u32; 8],
flags: u32,
) -> [u32; 8] {
parent_output(left_child_cv, right_child_cv, key_words, flags).chaining_value()
}
/// An incremental hasher that can accept any number of writes.
pub struct Hasher {
chunk_state: ChunkState,
key_words: [u32; 8],
cv_stack: [[u32; 8]; 54], // Space for 54 subtree chaining values:
cv_stack_len: u8, // 2^54 * CHUNK_LEN = 2^64
flags: u32,
}
impl Hasher {
fn new_internal(key_words: [u32; 8], flags: u32) -> Self {
Self {
chunk_state: ChunkState::new(key_words, 0, flags),
key_words,
cv_stack: [[0; 8]; 54],
cv_stack_len: 0,
flags,
}
}
/// Construct a new `Hasher` for the regular hash function.
pub fn new() -> Self {
Self::new_internal(IV, 0)
}
fn push_stack(&mut self, cv: [u32; 8]) {
self.cv_stack[self.cv_stack_len as usize] = cv;
self.cv_stack_len += 1;
}
fn pop_stack(&mut self) -> [u32; 8] {
self.cv_stack_len -= 1;
self.cv_stack[self.cv_stack_len as usize]
}
// Section 5.1.2 of the BLAKE3 spec explains this algorithm in more detail.
fn add_chunk_chaining_value(&mut self, mut new_cv: [u32; 8], mut total_chunks: u64) {
// This chunk might complete some subtrees. For each completed subtree,
// its left child will be the current top entry in the CV stack, and
// its right child will be the current value of `new_cv`. Pop each left
// child off the stack, merge it with `new_cv`, and overwrite `new_cv`
// with the result. After all these merges, push the final value of
// `new_cv` onto the stack. The number of completed subtrees is given
// by the number of trailing 0-bits in the new total number of chunks.
while total_chunks & 1 == 0 {
new_cv = parent_cv(self.pop_stack(), new_cv, self.key_words, self.flags);
total_chunks >>= 1;
}
self.push_stack(new_cv);
}
/// Add input to the hash state. This can be called any number of times.
pub fn update(&mut self, mut input: &[u8]) {
while !input.is_empty() {
// If the current chunk is complete, finalize it and reset the
// chunk state. More input is coming, so this chunk is not ROOT.
if self.chunk_state.len() == CHUNK_LEN {
let chunk_cv = self.chunk_state.output().chaining_value();
let total_chunks = self.chunk_state.chunk_counter + 1;
self.add_chunk_chaining_value(chunk_cv, total_chunks);
self.chunk_state = ChunkState::new(self.key_words, total_chunks, self.flags);
}
// Compress input bytes into the current chunk state.
let want = CHUNK_LEN - self.chunk_state.len();
let take = min(want, input.len());
self.chunk_state.update(&input[..take]);
input = &input[take..];
}
}
/// Finalize the hash and write any number of output bytes.
pub fn finalize(&self, out_slice: &mut [u8]) {
// Starting with the Output from the current chunk, compute all the
// parent chaining values along the right edge of the tree, until we
// have the root Output.
let mut output = self.chunk_state.output();
let mut parent_nodes_remaining = self.cv_stack_len as usize;
while parent_nodes_remaining > 0 {
parent_nodes_remaining -= 1;
output = parent_output(
self.cv_stack[parent_nodes_remaining],
output.chaining_value(),
self.key_words,
self.flags,
);
}
output.root_output_bytes(out_slice);
}
}
impl Default for Hasher {
fn default() -> Self {
Self::new()
}
}

11
crates/tauri-codegen/src/vendor/mod.rs vendored Normal file
View File

@@ -0,0 +1,11 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Manual vendored dependencies - NOT STABLE.
//!
//! This module and all submodules are not considered part of the public
//! api. They can and will change at any time for any reason in any
//! version.
pub mod blake3_reference;