use std::path::{Path, PathBuf};
use std::{ffi::OsStr, str::FromStr};
use base64::Engine;
use proc_macro2::TokenStream;
use quote::quote;
use sha2::{Digest, Sha256};
use tauri_utils::assets::AssetKey;
use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
use tauri_utils::html::{
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
};
#[cfg(feature = "shell-scope")]
use tauri_utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistScope};
use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
pub struct ContextData {
pub dev: bool,
pub config: Config,
pub config_parent: PathBuf,
pub root: TokenStream,
}
fn map_core_assets(
options: &AssetOptions,
target: Target,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
#[cfg(feature = "isolation")]
let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
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 mut document = parse_html(String::from_utf8_lossy(input).into_owned());
if target == Target::Linux {
::tauri_utils::html::inject_csp_token(&mut document);
}
inject_nonce_token(&mut document, &dangerous_disable_asset_csp_modification);
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
if let Ok(inline_script_elements) = document.select("script:not(empty)") {
let mut scripts = Vec::new();
for inline_script_el in inline_script_elements {
let script = inline_script_el.as_node().text_contents();
let mut hasher = Sha256::new();
hasher.update(&script);
let hash = hasher.finalize();
scripts.push(format!(
"'sha256-{}'",
base64::engine::general_purpose::STANDARD.encode(hash)
));
}
csp_hashes
.inline_scripts
.entry(key.clone().into())
.or_default()
.append(&mut scripts);
}
}
#[cfg(feature = "isolation")]
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
let mut hasher = Sha256::new();
hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
let hash = hasher.finalize();
csp_hashes.styles.push(format!(
"'sha256-{}'",
base64::engine::general_purpose::STANDARD.encode(hash)
));
}
}
*input = serialize_html_node(&document);
}
}
Ok(())
}
}
#[cfg(feature = "isolation")]
fn map_isolation(
_options: &AssetOptions,
dir: PathBuf,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
move |_key, path, input, _csp_hashes| {
if path.extension() == Some(OsStr::new("html")) {
let mut isolation_html =
tauri_utils::html::parse(String::from_utf8_lossy(input).into_owned());
tauri_utils::html::inject_codegen_isolation_script(&mut isolation_html);
tauri_utils::html::inline_isolation(&mut isolation_html, &dir);
*input = isolation_html.to_string().as_bytes().to_vec()
}
Ok(())
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
enum Target {
Linux,
Windows,
Darwin,
Android,
Ios,
}
pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsError> {
let ContextData {
dev,
config,
config_parent,
root,
} = data;
let target = if let Ok(target) = std::env::var("TARGET") {
if target.contains("unknown-linux") {
Target::Linux
} else if target.contains("pc-windows") {
Target::Windows
} else if target.contains("apple-darwin") {
Target::Darwin
} else if target.contains("android") {
Target::Android
} else if target.contains("apple-ios") {
Target::Ios
} else {
panic!("unknown codegen target {target}");
}
} else if cfg!(target_os = "linux") {
Target::Linux
} else if cfg!(windows) {
Target::Windows
} else if cfg!(target_os = "macos") {
Target::Darwin
} else if cfg!(target_os = "android") {
Target::Android
} else if cfg!(target_os = "ios") {
Target::Ios
} else {
panic!("unknown codegen target");
};
let mut options = AssetOptions::new(config.tauri.pattern.clone())
.freeze_prototype(config.tauri.security.freeze_prototype)
.dangerous_disable_asset_csp_modification(
config
.tauri
.security
.dangerous_disable_asset_csp_modification
.clone(),
);
let csp = if dev {
config
.tauri
.security
.dev_csp
.as_ref()
.or(config.tauri.security.csp.as_ref())
} else {
config.tauri.security.csp.as_ref()
};
if csp.is_some() {
options = options.with_csp();
}
let app_url = if dev {
&config.build.dev_path
} else {
&config.build.dist_dir
};
let assets = match app_url {
AppUrl::Url(url) => match url {
WindowUrl::External(_) => Default::default(),
WindowUrl::App(path) => {
if path.components().count() == 0 {
panic!(
"The `{}` configuration cannot be empty",
if dev { "devPath" } else { "distDir" }
)
}
let assets_path = config_parent.join(path);
if !assets_path.exists() {
panic!(
"The `{}` configuration is set to `{:?}` but this path doesn't exist",
if dev { "devPath" } else { "distDir" },
path
)
}
EmbeddedAssets::new(assets_path, &options, map_core_assets(&options, target))?
}
_ => unimplemented!(),
},
AppUrl::Files(files) => EmbeddedAssets::new(
files
.iter()
.map(|p| config_parent.join(p))
.collect::<Vec<_>>(),
&options,
map_core_assets(&options, target),
)?,
_ => unimplemented!(),
};
let out_dir = {
let out_dir = std::env::var("OUT_DIR")
.map_err(|_| EmbeddedAssetsError::OutDir)
.map(PathBuf::from)
.and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?;
std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
out_dir
};
let default_window_icon = {
if target == Target::Windows {
let icon_path = find_icon(
&config,
&config_parent,
|i| i.ends_with(".ico"),
"icons/icon.ico",
);
if icon_path.exists() {
ico_icon(&root, &out_dir, icon_path)?
} else {
let icon_path = find_icon(
&config,
&config_parent,
|i| i.ends_with(".png"),
"icons/icon.png",
);
png_icon(&root, &out_dir, icon_path)?
}
} else if target == Target::Linux {
let icon_path = find_icon(
&config,
&config_parent,
|i| i.ends_with(".png"),
"icons/icon.png",
);
png_icon(&root, &out_dir, icon_path)?
} else {
quote!(None)
}
};
let app_icon = if target == Target::Darwin && 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",
);
}
raw_icon(&out_dir, icon_path)?
} else {
quote!(None)
};
let package_name = if let Some(product_name) = &config.package.product_name {
quote!(#product_name.to_string())
} else {
quote!(env!("CARGO_PKG_NAME").to_string())
};
let package_version = if let Some(version) = &config.package.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"),
}
);
let system_tray_icon = if let Some(tray) = &config.tauri.system_tray {
let system_tray_icon_path = config_parent.join(&tray.icon_path);
let ext = system_tray_icon_path.extension();
if ext.map_or(false, |e| e == "ico") {
ico_icon(&root, &out_dir, system_tray_icon_path)?
} else if ext.map_or(false, |e| e == "png") {
png_icon(&root, &out_dir, system_tray_icon_path)?
} else {
quote!(compile_error!(
"The tray icon extension must be either `.ico` or `.png`."
))
}
} else {
quote!(None)
};
#[cfg(target_os = "macos")]
let info_plist = if target == Target::Darwin && dev {
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(product_name) = &config.package.product_name {
plist.insert("CFBundleName".into(), product_name.clone().into());
}
if let Some(version) = &config.package.version {
plist.insert("CFBundleShortVersionString".into(), version.clone().into());
}
let format =
time::format_description::parse("[year][month][day].[hour][minute][second]").unwrap();
if let Ok(build_number) = time::OffsetDateTime::now_utc().format(&format) {
plist.insert("CFBundleVersion".into(), build_number.into());
}
}
info_plist
.to_file_xml(out_dir.join("Info.plist"))
.expect("failed to write Info.plist");
quote!({
tauri::embed_plist::embed_info_plist!(concat!(std::env!("OUT_DIR"), "/Info.plist"));
})
} else {
quote!(())
};
#[cfg(not(target_os = "macos"))]
let info_plist = quote!(());
let pattern = match &options.pattern {
PatternKind::Brownfield => quote!(#root::Pattern::Brownfield(std::marker::PhantomData)),
#[cfg(not(feature = "isolation"))]
PatternKind::Isolation { dir: _ } => {
quote!(#root::Pattern::Brownfield(std::marker::PhantomData))
}
#[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| {
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")),
})
}
};
#[cfg(feature = "shell-scope")]
let shell_scope_config = {
use regex::Regex;
use tauri_utils::config::ShellAllowlistOpen;
let shell_scopes = get_allowed_clis(&root, &config.tauri.allowlist.shell.scope);
let shell_scope_open = match &config.tauri.allowlist.shell.open {
ShellAllowlistOpen::Flag(false) => quote!(::std::option::Option::None),
ShellAllowlistOpen::Flag(true) => {
quote!(::std::option::Option::Some(#root::regex::Regex::new(r#"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+"#).unwrap()))
}
ShellAllowlistOpen::Validate(regex) => match Regex::new(regex) {
Ok(_) => quote!(::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())),
Err(error) => {
let error = error.to_string();
quote!({
compile_error!(#error);
::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())
})
}
},
_ => panic!("unknown shell open format, unable to prepare"),
};
quote!(#root::ShellScopeConfig {
open: #shell_scope_open,
scopes: #shell_scopes
})
};
#[cfg(not(feature = "shell-scope"))]
let shell_scope_config = quote!();
let maybe_config_parent_setter = if dev {
let config_parent = config_parent.to_string_lossy();
quote!({
context.with_config_parent(#config_parent);
})
} else {
quote!()
};
Ok(quote!({
#[allow(unused_mut, clippy::let_and_return)]
let mut context = #root::Context::new(
#config,
::std::sync::Arc::new(#assets),
#default_window_icon,
#app_icon,
#system_tray_icon,
#package_info,
#info_plist,
#pattern,
#shell_scope_config
);
#maybe_config_parent_setter
context
}))
}
fn ico_icon<P: AsRef<Path>>(
root: &TokenStream,
out_dir: &Path,
path: P,
) -> Result<TokenStream, EmbeddedAssetsError> {
let path = path.as_ref();
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
.to_vec();
let icon_dir = ico::IconDir::read(std::io::Cursor::new(bytes))
.unwrap_or_else(|e| panic!("failed to parse icon {}: {}", path.display(), e));
let entry = &icon_dir.entries()[0];
let rgba = entry
.decode()
.unwrap_or_else(|e| panic!("failed to decode icon {}: {}", path.display(), e))
.rgba_data()
.to_vec();
let width = entry.width();
let height = entry.height();
let icon_file_name = path.file_name().unwrap();
let out_path = out_dir.join(icon_file_name);
write_if_changed(&out_path, &rgba).map_err(|error| EmbeddedAssetsError::AssetWrite {
path: path.to_owned(),
error,
})?;
let icon_file_name = icon_file_name.to_str().unwrap();
let icon = quote!(Some(
#root::Icon::Rgba {
rgba: include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_file_name)).to_vec(),
width: #width,
height: #height
}));
Ok(icon)
}
fn raw_icon<P: AsRef<Path>>(out_dir: &Path, path: P) -> Result<TokenStream, EmbeddedAssetsError> {
let path = path.as_ref();
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
.to_vec();
let out_path = out_dir.join(path.file_name().unwrap());
write_if_changed(&out_path, &bytes).map_err(|error| EmbeddedAssetsError::AssetWrite {
path: path.to_owned(),
error,
})?;
let icon_path = path.file_name().unwrap().to_str().unwrap().to_string();
let icon = quote!(::std::option::Option::Some(
include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_path)).to_vec()
));
Ok(icon)
}
fn png_icon<P: AsRef<Path>>(
root: &TokenStream,
out_dir: &Path,
path: P,
) -> Result<TokenStream, EmbeddedAssetsError> {
let path = path.as_ref();
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
.to_vec();
let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
let mut reader = decoder
.read_info()
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e));
let (color_type, _) = reader.output_color_type();
if color_type != png::ColorType::Rgba {
panic!("icon {} is not RGBA", path.display());
}
let mut buffer: Vec<u8> = Vec::new();
while let Ok(Some(row)) = reader.next_row() {
buffer.extend(row.data());
}
let width = reader.info().width;
let height = reader.info().height;
let icon_file_name = path.file_name().unwrap();
let out_path = out_dir.join(icon_file_name);
write_if_changed(&out_path, &buffer).map_err(|error| EmbeddedAssetsError::AssetWrite {
path: path.to_owned(),
error,
})?;
let icon_file_name = icon_file_name.to_str().unwrap();
let icon = quote!(Some(
#root::Icon::Rgba {
rgba: include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_file_name)).to_vec(),
width: #width,
height: #height,
}
));
Ok(icon)
}
fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> {
use std::fs::File;
use std::io::Write;
if let Ok(curr) = std::fs::read(out_path) {
if curr == data {
return Ok(());
}
}
let mut out_file = File::create(out_path)?;
out_file.write_all(data)
}
fn find_icon<F: Fn(&&String) -> bool>(
config: &Config,
config_parent: &Path,
predicate: F,
default: &str,
) -> PathBuf {
let icon_path = config
.tauri
.bundle
.icon
.iter()
.find(|i| predicate(i))
.cloned()
.unwrap_or_else(|| default.to_string());
config_parent.join(icon_path)
}
#[cfg(feature = "shell-scope")]
fn get_allowed_clis(root: &TokenStream, scope: &ShellAllowlistScope) -> TokenStream {
let commands = scope
.0
.iter()
.map(|scope| {
let sidecar = &scope.sidecar;
let name = &scope.name;
let name = quote!(#name.into());
let command = scope.command.to_string_lossy();
let command = quote!(::std::path::PathBuf::from(#command));
let args = match &scope.args {
ShellAllowedArgs::Flag(true) => quote!(::std::option::Option::None),
ShellAllowedArgs::Flag(false) => quote!(::std::option::Option::Some(::std::vec![])),
ShellAllowedArgs::List(list) => {
let list = list.iter().map(|arg| match arg {
ShellAllowedArg::Fixed(fixed) => {
quote!(#root::scope::ShellScopeAllowedArg::Fixed(#fixed.into()))
}
ShellAllowedArg::Var { validator } => {
let validator = match regex::Regex::new(validator) {
Ok(regex) => {
let regex = regex.as_str();
quote!(#root::regex::Regex::new(#regex).unwrap())
}
Err(error) => {
let error = error.to_string();
quote!({
compile_error!(#error);
#root::regex::Regex::new(#validator).unwrap()
})
}
};
quote!(#root::scope::ShellScopeAllowedArg::Var { validator: #validator })
}
_ => panic!("unknown shell scope arg, unable to prepare"),
});
quote!(::std::option::Option::Some(::std::vec![#(#list),*]))
}
_ => panic!("unknown shell scope command, unable to prepare"),
};
(
quote!(#name),
quote!(
#root::scope::ShellScopeAllowedCommand {
command: #command,
args: #args,
sidecar: #sidecar,
}
),
)
})
.collect::<Vec<_>>();
if commands.is_empty() {
quote!(::std::collections::HashMap::new())
} else {
let insertions = commands
.iter()
.map(|(name, value)| quote!(hashmap.insert(#name, #value);));
quote!({
let mut hashmap = ::std::collections::HashMap::new();
#(#insertions)*
hashmap
})
}
}