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
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:
493
crates/tauri-codegen/src/context.rs
Normal file
493
crates/tauri-codegen/src/context.rs
Normal 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)
|
||||
}
|
||||
452
crates/tauri-codegen/src/embedded_assets.rs
Normal file
452
crates/tauri-codegen/src/embedded_assets.rs
Normal 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)
|
||||
}
|
||||
208
crates/tauri-codegen/src/image.rs
Normal file
208
crates/tauri-codegen/src/image.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
152
crates/tauri-codegen/src/lib.rs
Normal file
152
crates/tauri-codegen/src/lib.rs
Normal 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)))
|
||||
}
|
||||
}
|
||||
378
crates/tauri-codegen/src/vendor/blake3_reference.rs
vendored
Normal file
378
crates/tauri-codegen/src/vendor/blake3_reference.rs
vendored
Normal 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
11
crates/tauri-codegen/src/vendor/mod.rs
vendored
Normal 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;
|
||||
Reference in New Issue
Block a user