(chore): update codebase
This commit is contained in:
parent
fd15215f15
commit
4e8d46b20e
106 changed files with 29936 additions and 1023 deletions
500
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/build.rs
vendored
Normal file
500
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/build.rs
vendored
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! ACL items that are only useful inside of build script/codegen context.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
acl::{AllowedCommands, Error, has_app_manifest},
|
||||
config::Config,
|
||||
write_if_changed,
|
||||
};
|
||||
|
||||
use super::{
|
||||
ALLOWED_COMMANDS_FILE_NAME, PERMISSION_SCHEMA_FILE_NAME, PERMISSION_SCHEMAS_FOLDER_NAME,
|
||||
REMOVE_UNUSED_COMMANDS_ENV_VAR,
|
||||
capability::{Capability, CapabilityFile},
|
||||
manifest::PermissionFile,
|
||||
};
|
||||
|
||||
/// Known name of the folder containing autogenerated permissions.
|
||||
pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated";
|
||||
|
||||
/// Cargo cfg key for permissions file paths
|
||||
pub const PERMISSION_FILES_PATH_KEY: &str = "PERMISSION_FILES_PATH";
|
||||
|
||||
/// Cargo cfg key for global scope schemas
|
||||
pub const GLOBAL_SCOPE_SCHEMA_PATH_KEY: &str = "GLOBAL_SCOPE_SCHEMA_PATH";
|
||||
|
||||
/// Allowed permission file extensions
|
||||
pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
|
||||
|
||||
/// Known filename of the permission documentation file
|
||||
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
|
||||
|
||||
/// Allowed capability file extensions
|
||||
const CAPABILITY_FILE_EXTENSIONS: &[&str] = &[
|
||||
"json",
|
||||
#[cfg(feature = "config-json5")]
|
||||
"json5",
|
||||
"toml",
|
||||
];
|
||||
|
||||
/// Known folder name of the capability schemas
|
||||
const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
|
||||
|
||||
const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
|
||||
|
||||
fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> {
|
||||
let mut permissions = Vec::new();
|
||||
for path in paths {
|
||||
let ext = path.extension().unwrap().to_string_lossy().to_string();
|
||||
let permission_file = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
|
||||
let permission: PermissionFile = match ext.as_str() {
|
||||
"toml" => toml::from_str(&permission_file)?,
|
||||
"json" => serde_json::from_str(&permission_file)?,
|
||||
_ => return Err(Error::UnknownPermissionFormat(ext)),
|
||||
};
|
||||
permissions.push(permission);
|
||||
}
|
||||
Ok(permissions)
|
||||
}
|
||||
|
||||
/// Write the permissions to a temporary directory and pass it to the immediate consuming crate.
|
||||
pub fn define_permissions<F: Fn(&Path) -> bool>(
|
||||
pattern: &str,
|
||||
pkg_name: &str,
|
||||
out_dir: &Path,
|
||||
filter_fn: F,
|
||||
) -> Result<Vec<PermissionFile>, Error> {
|
||||
let permission_files = glob::glob(pattern)?
|
||||
.flatten()
|
||||
.flat_map(|p| p.canonicalize())
|
||||
// filter extension
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| PERMISSION_FILE_EXTENSIONS.contains(&e))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.filter(|p| filter_fn(p))
|
||||
// filter schemas
|
||||
.filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME)
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
let pkg_name_valid_path = pkg_name.replace(':', "-");
|
||||
let permission_files_path = out_dir.join(format!("{pkg_name_valid_path}-permission-files"));
|
||||
let permission_files_json = serde_json::to_string(&permission_files)?;
|
||||
|
||||
write_if_changed(&permission_files_path, permission_files_json)
|
||||
.map_err(|e| Error::WriteFile(e, permission_files_path.clone()))?;
|
||||
|
||||
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
|
||||
println!(
|
||||
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{PERMISSION_FILES_PATH_KEY}={}",
|
||||
permission_files_path.display()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"cargo:{PERMISSION_FILES_PATH_KEY}={}",
|
||||
permission_files_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
parse_permissions(permission_files)
|
||||
}
|
||||
|
||||
/// Read all permissions listed from the defined cargo cfg key value.
|
||||
pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
|
||||
let mut permissions_map = HashMap::new();
|
||||
|
||||
for (key, value) in env::vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
|
||||
if let Some(plugin_crate_name_var) = key
|
||||
.strip_prefix("DEP_")
|
||||
.and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}")))
|
||||
.map(|v| {
|
||||
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
|
||||
.and_then(|v| v.strip_prefix("TAURI_"))
|
||||
.unwrap_or(v)
|
||||
})
|
||||
{
|
||||
let permissions_path = PathBuf::from(value);
|
||||
let permissions_str =
|
||||
fs::read_to_string(&permissions_path).map_err(|e| Error::ReadFile(e, permissions_path))?;
|
||||
let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
|
||||
let permissions = parse_permissions(permissions)?;
|
||||
|
||||
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
|
||||
let plugin_crate_name = plugin_crate_name
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or(plugin_crate_name);
|
||||
|
||||
permissions_map.insert(plugin_crate_name, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(permissions_map)
|
||||
}
|
||||
|
||||
/// Define the global scope schema JSON file path if it exists and pass it to the immediate consuming crate.
|
||||
pub fn define_global_scope_schema(
|
||||
schema: schemars::Schema,
|
||||
pkg_name: &str,
|
||||
out_dir: &Path,
|
||||
) -> Result<(), Error> {
|
||||
let path = out_dir.join("global-scope.json");
|
||||
write_if_changed(&path, serde_json::to_vec(&schema)?)
|
||||
.map_err(|e| Error::WriteFile(e, path.clone()))?;
|
||||
|
||||
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
|
||||
println!(
|
||||
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
println!("cargo:{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}", path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read all global scope schemas listed from the defined cargo cfg key value.
|
||||
pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> {
|
||||
let mut schemas_map = HashMap::new();
|
||||
|
||||
for (key, value) in env::vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
|
||||
if let Some(plugin_crate_name_var) = key
|
||||
.strip_prefix("DEP_")
|
||||
.and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}")))
|
||||
.map(|v| {
|
||||
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
|
||||
.and_then(|v| v.strip_prefix("TAURI_"))
|
||||
.unwrap_or(v)
|
||||
})
|
||||
{
|
||||
let path = PathBuf::from(value);
|
||||
let json = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
|
||||
let schema: serde_json::Value = serde_json::from_str(&json)?;
|
||||
|
||||
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
|
||||
let plugin_crate_name = plugin_crate_name
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or(plugin_crate_name);
|
||||
|
||||
schemas_map.insert(plugin_crate_name, schema);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(schemas_map)
|
||||
}
|
||||
|
||||
/// Parses all capability files with the given glob pattern.
|
||||
pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, Error> {
|
||||
let mut capabilities_map = BTreeMap::new();
|
||||
|
||||
for path in glob::glob(pattern)?
|
||||
.flatten() // filter extension
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| CAPABILITY_FILE_EXTENSIONS.contains(&e))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
// filter schema files
|
||||
// TODO: remove this before stable
|
||||
.filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME)
|
||||
{
|
||||
match CapabilityFile::load(&path)? {
|
||||
CapabilityFile::Capability(capability) => {
|
||||
if capabilities_map.contains_key(&capability.identifier) {
|
||||
return Err(Error::CapabilityAlreadyExists {
|
||||
identifier: capability.identifier,
|
||||
});
|
||||
}
|
||||
|
||||
capabilities_map.insert(capability.identifier.clone(), capability);
|
||||
}
|
||||
CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => {
|
||||
for capability in capabilities {
|
||||
if capabilities_map.contains_key(&capability.identifier) {
|
||||
return Err(Error::CapabilityAlreadyExists {
|
||||
identifier: capability.identifier,
|
||||
});
|
||||
}
|
||||
|
||||
capabilities_map.insert(capability.identifier.clone(), capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(capabilities_map)
|
||||
}
|
||||
|
||||
/// Permissions that are generated from commands using [`autogenerate_command_permissions`].
|
||||
pub struct AutogeneratedPermissions {
|
||||
/// The allow permissions generated from commands.
|
||||
pub allowed: Vec<String>,
|
||||
/// The deny permissions generated from commands.
|
||||
pub denied: Vec<String>,
|
||||
}
|
||||
|
||||
/// Autogenerate permission files for a list of commands.
|
||||
pub fn autogenerate_command_permissions(
|
||||
path: &Path,
|
||||
commands: &[&str],
|
||||
license_header: &str,
|
||||
schema_ref: bool,
|
||||
) -> AutogeneratedPermissions {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).expect("unable to create autogenerated commands dir");
|
||||
}
|
||||
|
||||
let schema_entry = if schema_ref {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count();
|
||||
let schema_path = (1..components_len)
|
||||
.map(|_| "..")
|
||||
.collect::<PathBuf>()
|
||||
.join(PERMISSION_SCHEMAS_FOLDER_NAME)
|
||||
.join(PERMISSION_SCHEMA_FILE_NAME);
|
||||
format!(
|
||||
"\n\"$schema\" = \"{}\"\n",
|
||||
dunce::simplified(&schema_path)
|
||||
.display()
|
||||
.to_string()
|
||||
.replace('\\', "/")
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let mut autogenerated = AutogeneratedPermissions {
|
||||
allowed: Vec::new(),
|
||||
denied: Vec::new(),
|
||||
};
|
||||
|
||||
for command in commands {
|
||||
let slugified_command = command.replace('_', "-");
|
||||
|
||||
let toml = format!(
|
||||
r###"{license_header}# Automatically generated - DO NOT EDIT!
|
||||
{schema_entry}
|
||||
[[permission]]
|
||||
identifier = "allow-{slugified_command}"
|
||||
description = "Enables the {command} command without any pre-configured scope."
|
||||
commands.allow = ["{command}"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-{slugified_command}"
|
||||
description = "Denies the {command} command without any pre-configured scope."
|
||||
commands.deny = ["{command}"]
|
||||
"###,
|
||||
);
|
||||
|
||||
let out_path = path.join(format!("{command}.toml"));
|
||||
write_if_changed(&out_path, toml)
|
||||
.unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}"));
|
||||
|
||||
autogenerated
|
||||
.allowed
|
||||
.push(format!("allow-{slugified_command}"));
|
||||
autogenerated
|
||||
.denied
|
||||
.push(format!("deny-{slugified_command}"));
|
||||
}
|
||||
|
||||
autogenerated
|
||||
}
|
||||
|
||||
const PERMISSION_TABLE_HEADER: &str =
|
||||
"## Permission Table\n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n";
|
||||
|
||||
/// Generate a markdown documentation page containing the list of permissions of the plugin.
|
||||
pub fn generate_docs(
|
||||
permissions: &[PermissionFile],
|
||||
out_dir: &Path,
|
||||
plugin_identifier: &str,
|
||||
) -> Result<(), Error> {
|
||||
let mut default_permission = "".to_owned();
|
||||
let mut permission_table = "".to_string();
|
||||
|
||||
fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String {
|
||||
let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n");
|
||||
if let Some(d) = description {
|
||||
docs.push_str(&format!("<td>\n\n{d}\n\n</td>"));
|
||||
}
|
||||
docs.push_str("\n</tr>");
|
||||
docs
|
||||
}
|
||||
|
||||
for permission in permissions {
|
||||
for set in &permission.set {
|
||||
permission_table.push_str(&docs_from(
|
||||
&set.identifier,
|
||||
Some(&set.description),
|
||||
plugin_identifier,
|
||||
));
|
||||
permission_table.push('\n');
|
||||
}
|
||||
|
||||
if let Some(default) = &permission.default {
|
||||
default_permission.push_str("## Default Permission\n\n");
|
||||
default_permission.push_str(default.description.as_deref().unwrap_or_default().trim());
|
||||
default_permission.push('\n');
|
||||
default_permission.push('\n');
|
||||
if !default.permissions.is_empty() {
|
||||
default_permission.push_str("#### This default permission set includes the following:\n\n");
|
||||
for permission in &default.permissions {
|
||||
default_permission.push_str(&format!("- `{permission}`\n"));
|
||||
}
|
||||
default_permission.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
for permission in &permission.permission {
|
||||
permission_table.push_str(&docs_from(
|
||||
&permission.identifier,
|
||||
permission.description.as_deref(),
|
||||
plugin_identifier,
|
||||
));
|
||||
permission_table.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let docs = format!("{default_permission}{PERMISSION_TABLE_HEADER}\n{permission_table}</table>\n");
|
||||
|
||||
let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
|
||||
write_if_changed(&reference_path, docs).map_err(|e| Error::WriteFile(e, reference_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: We have way too many duplicated code around getting the config files, e.g.
|
||||
// - crates/tauri-codegen/src/lib.rs (`get_config`)
|
||||
// - crates/tauri-build/src/lib.rs (`try_build`)
|
||||
// - crates/tauri-cli/src/helpers/config.rs (`get_internal`)
|
||||
/// Generate allowed commands file for the `generate_handler` macro to remove never allowed commands
|
||||
pub fn generate_allowed_commands(
|
||||
out_dir: &Path,
|
||||
capabilities_from_files: Option<BTreeMap<String, Capability>>,
|
||||
permissions_map: BTreeMap<String, Vec<PermissionFile>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
println!("cargo:rerun-if-env-changed={REMOVE_UNUSED_COMMANDS_ENV_VAR}");
|
||||
|
||||
let allowed_commands_file_path = out_dir.join(ALLOWED_COMMANDS_FILE_NAME);
|
||||
|
||||
let remove_unused_commands_env_var = std::env::var(REMOVE_UNUSED_COMMANDS_ENV_VAR);
|
||||
|
||||
let should_generate_allowed_commands =
|
||||
remove_unused_commands_env_var.is_ok() && !permissions_map.is_empty();
|
||||
|
||||
if !should_generate_allowed_commands {
|
||||
let _ = std::fs::remove_file(allowed_commands_file_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// It's safe to `unwrap` here since we have checked if the result is ok above
|
||||
let config_directory = PathBuf::from(remove_unused_commands_env_var.unwrap());
|
||||
let capabilities_path = config_directory.join("capabilities");
|
||||
// Cargo re-builds if the variable points to an empty path,
|
||||
// so we check for exists here
|
||||
// see https://github.com/rust-lang/cargo/issues/4213
|
||||
if capabilities_path.exists() {
|
||||
println!("cargo:rerun-if-changed={}", capabilities_path.display());
|
||||
}
|
||||
|
||||
let target_triple = env::var("TARGET")?;
|
||||
let target = crate::platform::Target::from_triple(&target_triple);
|
||||
let (mut config, config_paths) = crate::config::parse::read_from(target, &config_directory)?;
|
||||
|
||||
for config_file_path in config_paths {
|
||||
println!("cargo:rerun-if-changed={}", config_file_path.display());
|
||||
}
|
||||
|
||||
if let Ok(env) = std::env::var("TAURI_CONFIG") {
|
||||
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
|
||||
json_patch::merge(&mut config, &merge_config);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-env-changed=TAURI_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()?;
|
||||
std::env::set_current_dir(config_directory)?;
|
||||
|
||||
let config: Config = serde_json::from_value(config)?;
|
||||
|
||||
// Reset working directory.
|
||||
std::env::set_current_dir(old_cwd)?;
|
||||
|
||||
let acl: BTreeMap<String, crate::acl::manifest::Manifest> = permissions_map
|
||||
.into_iter()
|
||||
.map(|(key, permissions)| {
|
||||
let key = key
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.unwrap_or(&key)
|
||||
.to_string();
|
||||
let manifest = crate::acl::manifest::Manifest::new(permissions, None);
|
||||
(key, manifest)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let capabilities_from_files = if let Some(capabilities) = capabilities_from_files {
|
||||
capabilities
|
||||
} else {
|
||||
crate::acl::build::parse_capabilities(&format!(
|
||||
"{}/**/*",
|
||||
glob::Pattern::escape(&capabilities_path.to_string_lossy())
|
||||
))?
|
||||
};
|
||||
let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?;
|
||||
|
||||
let permission_entries = capabilities
|
||||
.into_values()
|
||||
.flat_map(|capabilities| capabilities.permissions);
|
||||
let mut allowed_commands = AllowedCommands {
|
||||
has_app_acl: has_app_manifest(&acl),
|
||||
..Default::default()
|
||||
};
|
||||
for permission_entry in permission_entries {
|
||||
let Ok(permissions) =
|
||||
crate::acl::resolved::get_permissions(permission_entry.identifier(), &acl)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for permission in permissions {
|
||||
let plugin_name = permission.key;
|
||||
let allowed_command_names = &permission.permission.commands.allow;
|
||||
for allowed_command in allowed_command_names {
|
||||
let command_name = if plugin_name == crate::acl::APP_ACL_KEY {
|
||||
allowed_command.to_string()
|
||||
} else if let Some(core_plugin_name) = plugin_name.strip_prefix("core:") {
|
||||
format!("plugin:{core_plugin_name}|{allowed_command}")
|
||||
} else {
|
||||
format!("plugin:{plugin_name}|{allowed_command}")
|
||||
};
|
||||
allowed_commands.commands.insert(command_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_if_changed(
|
||||
allowed_commands_file_path,
|
||||
serde_json::to_string(&allowed_commands)?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
452
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/capability.rs
vendored
Normal file
452
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/capability.rs
vendored
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
|
||||
|
||||
//! End-user abstraction for selecting permissions a window has access to.
|
||||
|
||||
use std::{path::Path, str::FromStr};
|
||||
|
||||
use crate::{acl::Identifier, platform::Target};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{Error, IntoDeserializer},
|
||||
};
|
||||
use serde_untagged::UntaggedEnumVisitor;
|
||||
|
||||
use super::Scopes;
|
||||
|
||||
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
|
||||
/// or an object that references a permission and extends its scope.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum PermissionEntry {
|
||||
/// Reference a permission or permission set by identifier.
|
||||
PermissionRef(Identifier),
|
||||
/// Reference a permission or permission set by identifier and extends its scope.
|
||||
ExtendedPermission {
|
||||
/// Identifier of the permission or permission set.
|
||||
identifier: Identifier,
|
||||
/// Scope to append to the existing permission scope.
|
||||
#[serde(default, flatten)]
|
||||
scope: Scopes,
|
||||
},
|
||||
}
|
||||
|
||||
impl PermissionEntry {
|
||||
/// The identifier of the permission referenced in this entry.
|
||||
pub fn identifier(&self) -> &Identifier {
|
||||
match self {
|
||||
Self::PermissionRef(identifier) => identifier,
|
||||
Self::ExtendedPermission {
|
||||
identifier,
|
||||
scope: _,
|
||||
} => identifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PermissionEntry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct ExtendedPermissionStruct {
|
||||
identifier: Identifier,
|
||||
#[serde(default, flatten)]
|
||||
scope: Scopes,
|
||||
}
|
||||
|
||||
UntaggedEnumVisitor::new()
|
||||
.string(|string| {
|
||||
let de = string.into_deserializer();
|
||||
Identifier::deserialize(de).map(Self::PermissionRef)
|
||||
})
|
||||
.map(|map| {
|
||||
let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
|
||||
Ok(Self::ExtendedPermission {
|
||||
identifier: ext_perm.identifier,
|
||||
scope: ext_perm.scope,
|
||||
})
|
||||
})
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
|
||||
///
|
||||
/// It controls application windows' and webviews' fine grained access
|
||||
/// to the Tauri core, application, or plugin commands.
|
||||
/// If a webview or its window is not matching any capability then it has no access to the IPC layer at all.
|
||||
///
|
||||
/// This can be done to create groups of windows, based on their required system access, which can reduce
|
||||
/// impact of frontend vulnerabilities in less privileged windows.
|
||||
/// Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.
|
||||
/// A Window can have none, one, or multiple associated capabilities.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "identifier": "main-user-files-write",
|
||||
/// "description": "This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
|
||||
/// "windows": [
|
||||
/// "main"
|
||||
/// ],
|
||||
/// "permissions": [
|
||||
/// "core:default",
|
||||
/// "dialog:open",
|
||||
/// {
|
||||
/// "identifier": "fs:allow-write-text-file",
|
||||
/// "allow": [{ "path": "$HOME/test.txt" }]
|
||||
/// },
|
||||
/// ],
|
||||
/// "platforms": ["macOS","windows"]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Capability {
|
||||
/// Identifier of the capability.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `main-user-files-write`
|
||||
///
|
||||
pub identifier: String,
|
||||
/// Description of what the capability is intended to allow on associated windows.
|
||||
///
|
||||
/// It should contain a description of what the grouped permissions should allow.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// This capability allows the `main` window access to `filesystem` write related
|
||||
/// commands and `dialog` commands to enable programmatic access to files selected by the user.
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Configure remote URLs that can use the capability permissions.
|
||||
///
|
||||
/// This setting is optional and defaults to not being set, as our
|
||||
/// default use case is that the content is served from our local application.
|
||||
///
|
||||
/// :::caution
|
||||
/// Make sure you understand the security implications of providing remote
|
||||
/// sources with local system access.
|
||||
/// :::
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "urls": ["https://*.mydomain.dev"]
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub remote: Option<CapabilityRemote>,
|
||||
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
|
||||
#[serde(default = "default_capability_local")]
|
||||
pub local: bool,
|
||||
/// List of windows that are affected by this capability. Can be a glob pattern.
|
||||
///
|
||||
/// If a window label matches any of the patterns in this list,
|
||||
/// the capability will be enabled on all the webviews of that window,
|
||||
/// regardless of the value of [`Self::webviews`].
|
||||
///
|
||||
/// On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`]
|
||||
/// for a fine grained access control.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `["main"]`
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub windows: Vec<String>,
|
||||
/// List of webviews that are affected by this capability. Can be a glob pattern.
|
||||
///
|
||||
/// The capability will be enabled on all the webviews
|
||||
/// whose label matches any of the patterns in this list,
|
||||
/// regardless of whether the webview's window label matches a pattern in [`Self::windows`].
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `["sub-webview-one", "sub-webview-two"]`
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub webviews: Vec<String>,
|
||||
/// List of permissions attached to this capability.
|
||||
///
|
||||
/// Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
|
||||
/// For commands directly implemented in the application itself only `${permission-name}`
|
||||
/// is required.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// [
|
||||
/// "core:default",
|
||||
/// "shell:allow-open",
|
||||
/// "dialog:open",
|
||||
/// {
|
||||
/// "identifier": "fs:allow-write-text-file",
|
||||
/// "allow": [{ "path": "$HOME/test.txt" }]
|
||||
/// }
|
||||
/// ]
|
||||
/// ```
|
||||
#[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
|
||||
pub permissions: Vec<PermissionEntry>,
|
||||
/// Limit which target platforms this capability applies to.
|
||||
///
|
||||
/// By default all platforms are targeted.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `["macOS","windows"]`
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub platforms: Option<Vec<Target>>,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
/// Whether this capability should be active based on the platform target or not.
|
||||
pub fn is_active(&self, target: &Target) -> bool {
|
||||
self
|
||||
.platforms
|
||||
.as_ref()
|
||||
.map(|platforms| platforms.contains(target))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
fn unique_permission(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let items = serde_json::Value::from(generator.subschema_for::<PermissionEntry>());
|
||||
schemars::json_schema!({
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": items
|
||||
})
|
||||
}
|
||||
|
||||
fn default_capability_local() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Configuration for remote URLs that are associated with the capability.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapabilityRemote {
|
||||
/// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// - "https://*.mydomain.dev": allows subdomains of mydomain.dev
|
||||
/// - "https://mydomain.dev/api/*": allows any subpath of mydomain.dev/api
|
||||
pub urls: Vec<String>,
|
||||
}
|
||||
|
||||
/// Capability formats accepted in a capability file.
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schema", schemars(untagged))]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub enum CapabilityFile {
|
||||
/// A single capability.
|
||||
Capability(Capability),
|
||||
/// A list of capabilities.
|
||||
List(Vec<Capability>),
|
||||
/// A list of capabilities.
|
||||
NamedList {
|
||||
/// The list of capabilities.
|
||||
capabilities: Vec<Capability>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CapabilityFile {
|
||||
/// Load the given capability file.
|
||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
|
||||
let path = path.as_ref();
|
||||
let capability_file =
|
||||
std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
|
||||
let ext = path.extension().unwrap().to_string_lossy().to_string();
|
||||
let file: Self = match ext.as_str() {
|
||||
"toml" => toml::from_str(&capability_file)?,
|
||||
"json" => serde_json::from_str(&capability_file)?,
|
||||
#[cfg(feature = "config-json5")]
|
||||
"json5" => json5::from_str(&capability_file)?,
|
||||
_ => return Err(super::Error::UnknownCapabilityFormat(ext)),
|
||||
};
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CapabilityFile {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
UntaggedEnumVisitor::new()
|
||||
.seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
|
||||
.map(|map| {
|
||||
#[derive(Deserialize)]
|
||||
struct CapabilityNamedList {
|
||||
capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
|
||||
if value.contains_key("capabilities") {
|
||||
serde_json::from_value::<CapabilityNamedList>(value.into())
|
||||
.map(|named| Self::NamedList {
|
||||
capabilities: named.capabilities,
|
||||
})
|
||||
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
|
||||
} else {
|
||||
serde_json::from_value::<Capability>(value.into())
|
||||
.map(Self::Capability)
|
||||
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
|
||||
}
|
||||
})
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CapabilityFile {
|
||||
type Err = super::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str(s)
|
||||
.or_else(|_| toml::from_str(s))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use std::convert::identity;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
impl ToTokens for CapabilityRemote {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let urls = vec_lit(&self.urls, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::capability::CapabilityRemote,
|
||||
urls
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PermissionEntry {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::PermissionRef(id) => {
|
||||
quote! { #prefix::PermissionRef(#id) }
|
||||
}
|
||||
Self::ExtendedPermission { identifier, scope } => {
|
||||
quote! { #prefix::ExtendedPermission {
|
||||
identifier: #identifier,
|
||||
scope: #scope
|
||||
} }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Capability {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let identifier = str_lit(&self.identifier);
|
||||
let description = str_lit(&self.description);
|
||||
let remote = opt_lit(self.remote.as_ref());
|
||||
let local = self.local;
|
||||
let windows = vec_lit(&self.windows, str_lit);
|
||||
let webviews = vec_lit(&self.webviews, str_lit);
|
||||
let permissions = vec_lit(&self.permissions, identity);
|
||||
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::capability::Capability,
|
||||
identifier,
|
||||
description,
|
||||
remote,
|
||||
local,
|
||||
windows,
|
||||
webviews,
|
||||
permissions,
|
||||
platforms
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::acl::{Identifier, Scopes};
|
||||
|
||||
use super::{Capability, CapabilityFile, PermissionEntry};
|
||||
|
||||
#[test]
|
||||
fn permission_entry_de() {
|
||||
let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
|
||||
let identifier_json = serde_json::to_string(&identifier).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
|
||||
PermissionEntry::PermissionRef(identifier.clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_value::<PermissionEntry>(serde_json::json!({
|
||||
"identifier": identifier,
|
||||
"allow": [],
|
||||
"deny": null
|
||||
}))
|
||||
.unwrap(),
|
||||
PermissionEntry::ExtendedPermission {
|
||||
identifier,
|
||||
scope: Scopes {
|
||||
allow: Some(vec![]),
|
||||
deny: None
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_file_de() {
|
||||
let capability = Capability {
|
||||
identifier: "test".into(),
|
||||
description: "".into(),
|
||||
remote: None,
|
||||
local: true,
|
||||
windows: vec![],
|
||||
webviews: vec![],
|
||||
permissions: vec![],
|
||||
platforms: None,
|
||||
};
|
||||
let capability_json = serde_json::to_string(&capability).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
|
||||
CapabilityFile::Capability(capability.clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
|
||||
CapabilityFile::List(vec![capability.clone()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&format!(
|
||||
"{{ \"capabilities\": [{capability_json}] }}"
|
||||
))
|
||||
.unwrap(),
|
||||
CapabilityFile::NamedList {
|
||||
capabilities: vec![capability]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
301
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/identifier.rs
vendored
Normal file
301
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/identifier.rs
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Identifier for plugins.
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::num::NonZeroU8;
|
||||
use thiserror::Error;
|
||||
|
||||
const IDENTIFIER_SEPARATOR: u8 = b':';
|
||||
const PLUGIN_PREFIX: &str = "tauri-plugin-";
|
||||
const CORE_PLUGIN_IDENTIFIER_PREFIX: &str = "core:";
|
||||
|
||||
// <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>
|
||||
const MAX_LEN_PREFIX: usize = 64 - PLUGIN_PREFIX.len();
|
||||
const MAX_LEN_BASE: usize = 64;
|
||||
const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE;
|
||||
|
||||
/// Plugin identifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Identifier {
|
||||
inner: String,
|
||||
separator: Option<NonZeroU8>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
impl schemars::JsonSchema for Identifier {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
"Identifier".into()
|
||||
}
|
||||
|
||||
fn schema_id() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed(concat!(module_path!(), "::Identifier"))
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
String::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Identifier {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier {
|
||||
/// Get the identifier str.
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
/// Get the identifier without prefix.
|
||||
pub fn get_base(&self) -> &str {
|
||||
match self.separator_index() {
|
||||
None => self.get(),
|
||||
Some(i) => &self.inner[i + 1..],
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the prefix of the identifier.
|
||||
pub fn get_prefix(&self) -> Option<&str> {
|
||||
self.separator_index().map(|i| &self.inner[0..i])
|
||||
}
|
||||
|
||||
/// Set the identifier prefix.
|
||||
pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the identifier string and its separator.
|
||||
pub fn into_inner(self) -> (String, Option<NonZeroU8>) {
|
||||
(self.inner, self.separator)
|
||||
}
|
||||
|
||||
fn separator_index(&self) -> Option<usize> {
|
||||
self.separator.map(|i| i.get() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ValidByte {
|
||||
Separator,
|
||||
Byte(u8),
|
||||
}
|
||||
|
||||
impl ValidByte {
|
||||
fn alpha_numeric(byte: u8) -> Option<Self> {
|
||||
byte.is_ascii_alphanumeric().then_some(Self::Byte(byte))
|
||||
}
|
||||
|
||||
fn alpha_numeric_hyphen(byte: u8) -> Option<Self> {
|
||||
(byte.is_ascii_alphanumeric() || byte == b'-').then_some(Self::Byte(byte))
|
||||
}
|
||||
|
||||
fn next(&self, next: u8) -> Option<ValidByte> {
|
||||
match (self, next) {
|
||||
(ValidByte::Byte(b'-'), IDENTIFIER_SEPARATOR) => None,
|
||||
(ValidByte::Separator, b'-') => None,
|
||||
|
||||
(_, IDENTIFIER_SEPARATOR) => Some(ValidByte::Separator),
|
||||
(ValidByte::Separator, next) => ValidByte::alpha_numeric(next),
|
||||
(ValidByte::Byte(b'-'), next) => ValidByte::alpha_numeric_hyphen(next),
|
||||
(ValidByte::Byte(b'_'), next) => ValidByte::alpha_numeric_hyphen(next),
|
||||
(ValidByte::Byte(_), next) => ValidByte::alpha_numeric_hyphen(next),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can happen when parsing an identifier.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseIdentifierError {
|
||||
/// Identifier start with the plugin prefix.
|
||||
#[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
|
||||
StartsWithTauriPlugin,
|
||||
|
||||
/// Identifier empty.
|
||||
#[error("identifiers cannot be empty")]
|
||||
Empty,
|
||||
|
||||
/// Identifier is too long.
|
||||
#[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
|
||||
Humongous(usize),
|
||||
|
||||
/// Identifier is not in a valid format.
|
||||
#[error(
|
||||
"identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix"
|
||||
)]
|
||||
InvalidFormat,
|
||||
|
||||
/// Identifier has multiple separators.
|
||||
#[error(
|
||||
"identifiers can only include a single separator '{}'",
|
||||
IDENTIFIER_SEPARATOR
|
||||
)]
|
||||
MultipleSeparators,
|
||||
|
||||
/// Identifier has a trailing hyphen.
|
||||
#[error("identifiers cannot have a trailing hyphen")]
|
||||
TrailingHyphen,
|
||||
|
||||
/// Identifier has a prefix without a base.
|
||||
#[error("identifiers cannot have a prefix without a base")]
|
||||
PrefixWithoutBase,
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Identifier {
|
||||
type Error = ParseIdentifierError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if value.starts_with(PLUGIN_PREFIX) {
|
||||
return Err(Self::Error::StartsWithTauriPlugin);
|
||||
}
|
||||
|
||||
if value.is_empty() {
|
||||
return Err(Self::Error::Empty);
|
||||
}
|
||||
|
||||
if value.len() > MAX_LEN_IDENTIFIER {
|
||||
return Err(Self::Error::Humongous(value.len()));
|
||||
}
|
||||
|
||||
let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
|
||||
|
||||
let mut bytes = value.bytes();
|
||||
|
||||
// grab the first byte only before parsing the rest
|
||||
let mut prev = bytes
|
||||
.next()
|
||||
.and_then(ValidByte::alpha_numeric)
|
||||
.ok_or(Self::Error::InvalidFormat)?;
|
||||
|
||||
let mut idx = 0;
|
||||
let mut separator = None;
|
||||
for byte in bytes {
|
||||
idx += 1; // we already consumed first item
|
||||
match prev.next(byte) {
|
||||
None => return Err(Self::Error::InvalidFormat),
|
||||
Some(next @ ValidByte::Byte(_)) => prev = next,
|
||||
Some(ValidByte::Separator) => {
|
||||
if separator.is_none() || is_core_identifier {
|
||||
// safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
|
||||
separator = Some(idx.try_into().unwrap());
|
||||
prev = ValidByte::Separator
|
||||
} else {
|
||||
return Err(Self::Error::MultipleSeparators);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match prev {
|
||||
// empty base
|
||||
ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
|
||||
|
||||
// trailing hyphen
|
||||
ValidByte::Byte(b'-') => return Err(Self::Error::TrailingHyphen),
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: value,
|
||||
separator,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Identifier {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Identifier {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.get())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ident(s: impl Into<String>) -> Result<Identifier, ParseIdentifierError> {
|
||||
Identifier::try_from(s.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_len_fits_in_u8() {
|
||||
assert!(MAX_LEN_IDENTIFIER < u8::MAX as usize)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format() {
|
||||
assert!(ident("prefix:base").is_ok());
|
||||
assert!(ident("prefix3:base").is_ok());
|
||||
assert!(ident("preFix:base").is_ok());
|
||||
|
||||
// bad
|
||||
assert!(ident("tauri-plugin-prefix:base").is_err());
|
||||
|
||||
assert!(ident("-prefix-:-base-").is_err());
|
||||
assert!(ident("-prefix:base").is_err());
|
||||
assert!(ident("prefix-:base").is_err());
|
||||
assert!(ident("prefix:-base").is_err());
|
||||
assert!(ident("prefix:base-").is_err());
|
||||
|
||||
assert!(ident("pre--fix:base--sep").is_ok());
|
||||
assert!(ident("prefix:base--sep").is_ok());
|
||||
assert!(ident("pre--fix:base").is_ok());
|
||||
|
||||
assert!(ident("prefix::base").is_err());
|
||||
assert!(ident(":base").is_err());
|
||||
assert!(ident("prefix:").is_err());
|
||||
assert!(ident(":prefix:base:").is_err());
|
||||
assert!(ident("base:").is_err());
|
||||
|
||||
assert!(ident("").is_err());
|
||||
assert!(ident("💩").is_err());
|
||||
|
||||
assert!(ident("a".repeat(MAX_LEN_IDENTIFIER + 1)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base() {
|
||||
assert_eq!(ident("prefix:base").unwrap().get_base(), "base");
|
||||
assert_eq!(ident("base").unwrap().get_base(), "base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix() {
|
||||
assert_eq!(ident("prefix:base").unwrap().get_prefix(), Some("prefix"));
|
||||
assert_eq!(ident("base").unwrap().get_prefix(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ToTokens for Identifier {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let s = self.get();
|
||||
tokens
|
||||
.append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
|
||||
}
|
||||
}
|
||||
}
|
||||
196
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/manifest.rs
vendored
Normal file
196
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/manifest.rs
vendored
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Plugin ACL types.
|
||||
|
||||
use std::{collections::BTreeMap, num::NonZeroU64};
|
||||
|
||||
use super::{Permission, PermissionSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The default permission set of the plugin.
|
||||
///
|
||||
/// Works similarly to a permission with the "default" identifier.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct DefaultPermission {
|
||||
/// The version of the permission.
|
||||
pub version: Option<NonZeroU64>,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
/// Tauri convention is to use `<h4>` headings in markdown content
|
||||
/// for Tauri documentation generation purposes.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// All permissions this set contains.
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct PermissionFile {
|
||||
/// The default permission set for the plugin
|
||||
pub default: Option<DefaultPermission>,
|
||||
|
||||
/// A list of permissions sets defined
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub set: Vec<PermissionSet>,
|
||||
|
||||
/// A list of inlined permissions
|
||||
#[serde(default)]
|
||||
pub permission: Vec<Permission>,
|
||||
}
|
||||
|
||||
/// Plugin manifest.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Manifest {
|
||||
/// Default permission.
|
||||
pub default_permission: Option<PermissionSet>,
|
||||
/// Plugin permissions.
|
||||
pub permissions: BTreeMap<String, Permission>,
|
||||
/// Plugin permission sets.
|
||||
pub permission_sets: BTreeMap<String, PermissionSet>,
|
||||
/// The global scope schema.
|
||||
pub global_scope_schema: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
/// Creates a new manifest from the given plugin permission files and global scope schema.
|
||||
pub fn new(
|
||||
permission_files: Vec<PermissionFile>,
|
||||
global_scope_schema: Option<serde_json::Value>,
|
||||
) -> Self {
|
||||
let mut manifest = Self {
|
||||
default_permission: None,
|
||||
permissions: BTreeMap::new(),
|
||||
permission_sets: BTreeMap::new(),
|
||||
global_scope_schema,
|
||||
};
|
||||
|
||||
for permission_file in permission_files {
|
||||
if let Some(default) = permission_file.default {
|
||||
manifest.default_permission.replace(PermissionSet {
|
||||
identifier: "default".into(),
|
||||
description: default
|
||||
.description
|
||||
.unwrap_or_else(|| "Default plugin permissions.".to_string()),
|
||||
permissions: default.permissions,
|
||||
});
|
||||
}
|
||||
|
||||
for permission in permission_file.permission {
|
||||
let key = permission.identifier.clone();
|
||||
manifest.permissions.insert(key, permission);
|
||||
}
|
||||
|
||||
for set in permission_file.set {
|
||||
let key = set.identifier.clone();
|
||||
manifest.permission_sets.insert(key, set);
|
||||
}
|
||||
}
|
||||
|
||||
manifest
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
type ScopeSchema = (schemars::Schema, serde_json::Map<String, serde_json::Value>);
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
impl Manifest {
|
||||
/// Return scope schema and extra schema definitions for this plugin manifest.
|
||||
pub fn global_scope_schema(&self) -> Result<Option<ScopeSchema>, super::Error> {
|
||||
self
|
||||
.global_scope_schema
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
serde_json::from_value::<schemars::Schema>(s.clone()).map(|mut root| {
|
||||
// Extract definitions from the schema
|
||||
let definitions = root
|
||||
.remove("$defs")
|
||||
.or_else(|| root.remove("definitions"))
|
||||
.and_then(|v| match v {
|
||||
serde_json::Value::Object(m) => Some(m),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Wrap in an array schema
|
||||
let items = serde_json::Value::from(root);
|
||||
let scope_schema = schemars::json_schema!({
|
||||
"type": "array",
|
||||
"items": items
|
||||
});
|
||||
|
||||
(scope_schema, definitions)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
use std::convert::identity;
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
impl ToTokens for DefaultPermission {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let version = opt_lit_owned(self.version.as_ref().map(|v| {
|
||||
let v = v.get();
|
||||
quote!(::core::num::NonZeroU64::new(#v).unwrap())
|
||||
}));
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
let description = quote! { ::core::option::Option::None };
|
||||
let permissions = vec_lit(&self.permissions, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::plugin::DefaultPermission,
|
||||
version,
|
||||
description,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Manifest {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default_permission = opt_lit(self.default_permission.as_ref());
|
||||
|
||||
let permissions = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.permissions,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
let permission_sets = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.permission_sets,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
// let global_scope_schema =
|
||||
// opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit));
|
||||
let global_scope_schema = quote! { ::core::option::Option::None };
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::manifest::Manifest,
|
||||
default_permission,
|
||||
permissions,
|
||||
permission_sets,
|
||||
global_scope_schema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
550
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/mod.rs
vendored
Normal file
550
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Access Control List types.
|
||||
//!
|
||||
//! # Stability
|
||||
//!
|
||||
//! This is a core functionality that is not considered part of the stable API.
|
||||
//! If you use it, note that it may include breaking changes in the future.
|
||||
//!
|
||||
//! These items are intended to be non-breaking from a de/serialization standpoint only.
|
||||
//! Using and modifying existing config values will try to avoid breaking changes, but they are
|
||||
//! free to add fields in the future - causing breaking changes for creating and full destructuring.
|
||||
//!
|
||||
//! To avoid this, [ignore unknown fields when destructuring] with the `{my, config, ..}` pattern.
|
||||
//! If you need to create the Rust config directly without deserializing, then create the struct
|
||||
//! the [Struct Update Syntax] with `..Default::default()`, which may need a
|
||||
//! `#[allow(clippy::needless_update)]` attribute if you are declaring all fields.
|
||||
//!
|
||||
//! [ignore unknown fields when destructuring]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-remaining-parts-of-a-value-with-
|
||||
//! [Struct Update Syntax]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
|
||||
|
||||
use anyhow::Context;
|
||||
use capability::{Capability, CapabilityFile};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
fs,
|
||||
num::NonZeroU64,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
config::{CapabilityEntry, Config},
|
||||
platform::Target,
|
||||
};
|
||||
|
||||
pub use self::{identifier::*, value::*};
|
||||
|
||||
/// Known foldername of the permission schema files
|
||||
pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
|
||||
/// Known filename of the permission schema JSON file
|
||||
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
|
||||
/// Known ACL key for the app permissions.
|
||||
pub const APP_ACL_KEY: &str = "__app-acl__";
|
||||
/// Known acl manifests file
|
||||
pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
|
||||
/// Known capabilities file
|
||||
pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
|
||||
/// Allowed commands file name
|
||||
pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json";
|
||||
/// Set by the CLI with when `build > removeUnusedCommands` is set for dead code elimination,
|
||||
/// the value is set to the config's directory
|
||||
pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS";
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub mod build;
|
||||
pub mod capability;
|
||||
pub mod identifier;
|
||||
pub mod manifest;
|
||||
pub mod resolved;
|
||||
#[cfg(feature = "schema")]
|
||||
pub mod schema;
|
||||
pub mod value;
|
||||
|
||||
/// Possible errors while processing ACL files.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Could not find an environmental variable that is set inside of build scripts.
|
||||
///
|
||||
/// Whatever generated this should be called inside of a build script.
|
||||
#[error(
|
||||
"expected build script env var {0}, but it was not found - ensure this is called in a build script"
|
||||
)]
|
||||
BuildVar(&'static str),
|
||||
|
||||
/// The links field in the manifest **MUST** be set and match the name of the crate.
|
||||
#[error(
|
||||
"package.links field in the Cargo manifest is not set, it should be set to the same as package.name"
|
||||
)]
|
||||
LinksMissing,
|
||||
|
||||
/// The links field in the manifest **MUST** match the name of the crate.
|
||||
#[error(
|
||||
"package.links field in the Cargo manifest MUST be set to the same value as package.name"
|
||||
)]
|
||||
LinksName,
|
||||
|
||||
/// IO error while reading a file
|
||||
#[error("failed to read file '{}': {}", _1.display(), _0)]
|
||||
ReadFile(std::io::Error, PathBuf),
|
||||
|
||||
/// IO error while writing a file
|
||||
#[error("failed to write file '{}': {}", _1.display(), _0)]
|
||||
WriteFile(std::io::Error, PathBuf),
|
||||
|
||||
/// IO error while creating a file
|
||||
#[error("failed to create file '{}': {}", _1.display(), _0)]
|
||||
CreateFile(std::io::Error, PathBuf),
|
||||
|
||||
/// IO error while creating a dir
|
||||
#[error("failed to create dir '{}': {}", _1.display(), _0)]
|
||||
CreateDir(std::io::Error, PathBuf),
|
||||
|
||||
/// [`cargo_metadata`] was not able to complete successfully
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
#[error("failed to execute: {0}")]
|
||||
Metadata(#[from] ::cargo_metadata::Error),
|
||||
|
||||
/// Invalid glob
|
||||
#[error("failed to run glob: {0}")]
|
||||
Glob(#[from] glob::PatternError),
|
||||
|
||||
/// Invalid TOML encountered
|
||||
#[error("failed to parse TOML: {0}")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
|
||||
/// Invalid JSON encountered
|
||||
#[error("failed to parse JSON: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
/// Invalid JSON5 encountered
|
||||
#[cfg(feature = "config-json5")]
|
||||
#[error("failed to parse JSON5: {0}")]
|
||||
Json5(#[from] json5::Error),
|
||||
|
||||
/// Invalid permissions file format
|
||||
#[error("unknown permission format {0}")]
|
||||
UnknownPermissionFormat(String),
|
||||
|
||||
/// Invalid capabilities file format
|
||||
#[error("unknown capability format {0}")]
|
||||
UnknownCapabilityFormat(String),
|
||||
|
||||
/// Permission referenced in set not found.
|
||||
#[error("permission {permission} not found from set {set}")]
|
||||
SetPermissionNotFound {
|
||||
/// Permission identifier.
|
||||
permission: String,
|
||||
/// Set identifier.
|
||||
set: String,
|
||||
},
|
||||
|
||||
/// Unknown ACL manifest.
|
||||
#[error("unknown ACL for {key}, expected one of {available}")]
|
||||
UnknownManifest {
|
||||
/// Manifest key.
|
||||
key: String,
|
||||
/// Available manifest keys.
|
||||
available: String,
|
||||
},
|
||||
|
||||
/// Unknown permission.
|
||||
#[error("unknown permission {permission} for {key}")]
|
||||
UnknownPermission {
|
||||
/// Manifest key.
|
||||
key: String,
|
||||
|
||||
/// Permission identifier.
|
||||
permission: String,
|
||||
},
|
||||
|
||||
/// Capability with the given identifier already exists.
|
||||
#[error("capability with identifier `{identifier}` already exists")]
|
||||
CapabilityAlreadyExists {
|
||||
/// Capability identifier.
|
||||
identifier: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Allowed and denied commands inside a permission.
|
||||
///
|
||||
/// If two commands clash inside of `allow` and `deny`, it should be denied by default.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Commands {
|
||||
/// Allowed command.
|
||||
#[serde(default)]
|
||||
pub allow: Vec<String>,
|
||||
|
||||
/// Denied command, which takes priority.
|
||||
#[serde(default)]
|
||||
pub deny: Vec<String>,
|
||||
}
|
||||
|
||||
/// An argument for fine grained behavior control of Tauri commands.
|
||||
///
|
||||
/// It can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.
|
||||
/// The configured scope is passed to the command and will be enforced by the command implementation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "allow": [{ "path": "$HOME/**" }],
|
||||
/// "deny": [{ "path": "$HOME/secret.txt" }]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Scopes {
|
||||
/// Data that defines what is allowed by the scope.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow: Option<Vec<Value>>,
|
||||
/// Data that defines what is denied by the scope. This should be prioritized by validation logic.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deny: Option<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Scopes {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.allow.is_none() && self.deny.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Descriptions of explicit privileges of commands.
|
||||
///
|
||||
/// It can enable commands to be accessible in the frontend of the application.
|
||||
///
|
||||
/// If the scope is defined it can be used to fine grain control the access of individual or multiple commands.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Permission {
|
||||
/// The version of the permission.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<NonZeroU64>,
|
||||
|
||||
/// A unique identifier for the permission.
|
||||
pub identifier: String,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
/// Tauri internal convention is to use `<h4>` headings in markdown content
|
||||
/// for Tauri documentation generation purposes.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Allowed or denied commands when using this permission.
|
||||
#[serde(default)]
|
||||
pub commands: Commands,
|
||||
|
||||
/// Allowed or denied scoped when using this permission.
|
||||
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
|
||||
pub scope: Scopes,
|
||||
|
||||
/// Target platforms this permission applies. By default all platforms are affected by this permission.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub platforms: Option<Vec<Target>>,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
/// Whether this permission should be active based on the platform target or not.
|
||||
pub fn is_active(&self, target: &Target) -> bool {
|
||||
self
|
||||
.platforms
|
||||
.as_ref()
|
||||
.map(|platforms| platforms.contains(target))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of direct permissions grouped together under a new name.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct PermissionSet {
|
||||
/// A unique identifier for the permission.
|
||||
pub identifier: String,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
pub description: String,
|
||||
|
||||
/// All permissions this set contains.
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
/// UrlPattern for [`ExecutionContext::Remote`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
|
||||
|
||||
impl FromStr for RemoteUrlPattern {
|
||||
type Err = urlpattern::quirks::Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
|
||||
if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
|
||||
init.search.replace("*".to_string());
|
||||
}
|
||||
if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
|
||||
init.hash.replace("*".to_string());
|
||||
}
|
||||
if init
|
||||
.pathname
|
||||
.as_ref()
|
||||
.map(|p| p.is_empty() || p == "/")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
init.pathname.replace("*".to_string());
|
||||
}
|
||||
let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
|
||||
Ok(Self(Arc::new(pattern), s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteUrlPattern {
|
||||
#[doc(hidden)]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Test if a given URL matches the pattern.
|
||||
pub fn test(&self, url: &Url) -> bool {
|
||||
self
|
||||
.0
|
||||
.test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RemoteUrlPattern {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.protocol() == other.0.protocol()
|
||||
&& self.0.username() == other.0.username()
|
||||
&& self.0.password() == other.0.password()
|
||||
&& self.0.hostname() == other.0.hostname()
|
||||
&& self.0.port() == other.0.port()
|
||||
&& self.0.pathname() == other.0.pathname()
|
||||
&& self.0.search() == other.0.search()
|
||||
&& self.0.hash() == other.0.hash()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RemoteUrlPattern {}
|
||||
|
||||
/// Execution context of an IPC call.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub enum ExecutionContext {
|
||||
/// A local URL is used (the Tauri app URL).
|
||||
#[default]
|
||||
Local,
|
||||
/// Remote URL is trying to use the IPC.
|
||||
Remote {
|
||||
/// The URL trying to access the IPC (URL pattern).
|
||||
url: RemoteUrlPattern,
|
||||
},
|
||||
}
|
||||
|
||||
/// Test if the app has an application manifest from the ACL
|
||||
pub fn has_app_manifest(acl: &BTreeMap<String, crate::acl::manifest::Manifest>) -> bool {
|
||||
acl.contains_key(APP_ACL_KEY)
|
||||
}
|
||||
|
||||
/// Get the capabilities from the config file
|
||||
pub fn get_capabilities(
|
||||
config: &Config,
|
||||
mut capabilities_from_files: BTreeMap<String, Capability>,
|
||||
additional_capability_files: Option<&[PathBuf]>,
|
||||
) -> anyhow::Result<BTreeMap<String, Capability>> {
|
||||
let mut capabilities = if config.app.security.capabilities.is_empty() {
|
||||
capabilities_from_files
|
||||
} else {
|
||||
let mut capabilities = BTreeMap::new();
|
||||
for capability_entry in &config.app.security.capabilities {
|
||||
match capability_entry {
|
||||
CapabilityEntry::Inlined(capability) => {
|
||||
capabilities.insert(capability.identifier.clone(), capability.clone());
|
||||
}
|
||||
CapabilityEntry::Reference(id) => {
|
||||
let capability = capabilities_from_files
|
||||
.remove(id)
|
||||
.with_context(|| format!("capability with identifier {id} not found"))?;
|
||||
capabilities.insert(id.clone(), capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
capabilities
|
||||
};
|
||||
|
||||
if let Some(paths) = additional_capability_files {
|
||||
for path in paths {
|
||||
let capability = CapabilityFile::load(path)
|
||||
.with_context(|| format!("failed to read capability {}", path.display()))?;
|
||||
match capability {
|
||||
CapabilityFile::Capability(c) => {
|
||||
capabilities.insert(c.identifier.clone(), c);
|
||||
}
|
||||
CapabilityFile::List(capabilities_list)
|
||||
| CapabilityFile::NamedList {
|
||||
capabilities: capabilities_list,
|
||||
} => {
|
||||
capabilities.extend(
|
||||
capabilities_list
|
||||
.into_iter()
|
||||
.map(|c| (c.identifier.clone(), c)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
/// Allowed commands used to communicate between `generate_handle` and `generate_allowed_commands` through json files
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct AllowedCommands {
|
||||
/// The commands allowed
|
||||
pub commands: HashSet<String>,
|
||||
/// Has application ACL or not
|
||||
pub has_app_acl: bool,
|
||||
}
|
||||
|
||||
/// Try to reads allowed commands from the out dir made by our build script
|
||||
pub fn read_allowed_commands() -> Option<AllowedCommands> {
|
||||
let out_file = std::env::var("OUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.ok()?
|
||||
.join(ALLOWED_COMMANDS_FILE_NAME);
|
||||
let file = fs::read_to_string(&out_file).ok()?;
|
||||
let json = serde_json::from_str(&file).ok()?;
|
||||
Some(json)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::acl::RemoteUrlPattern;
|
||||
|
||||
#[test]
|
||||
fn url_pattern_domain_wildcard() {
|
||||
let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
|
||||
|
||||
assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
|
||||
|
||||
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
|
||||
|
||||
let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
|
||||
|
||||
assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
|
||||
assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
|
||||
assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_pattern_path_wildcard() {
|
||||
let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
|
||||
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_pattern_scheme_wildcard() {
|
||||
let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
|
||||
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
|
||||
assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build_ {
|
||||
use std::convert::identity;
|
||||
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
impl ToTokens for ExecutionContext {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::Local => {
|
||||
quote! { #prefix::Local }
|
||||
}
|
||||
Self::Remote { url } => {
|
||||
let url = url.as_str();
|
||||
quote! { #prefix::Remote { url: #url.parse().unwrap() } }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Commands {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = vec_lit(&self.allow, str_lit);
|
||||
let deny = vec_lit(&self.deny, str_lit);
|
||||
literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Scopes {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = opt_vec_lit(self.allow.as_ref(), identity);
|
||||
let deny = opt_vec_lit(self.deny.as_ref(), identity);
|
||||
literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Permission {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let version = opt_lit_owned(self.version.as_ref().map(|v| {
|
||||
let v = v.get();
|
||||
quote!(::core::num::NonZeroU64::new(#v).unwrap())
|
||||
}));
|
||||
let identifier = str_lit(&self.identifier);
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
let description = quote! { ::core::option::Option::None };
|
||||
let commands = &self.commands;
|
||||
let scope = &self.scope;
|
||||
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::Permission,
|
||||
version,
|
||||
identifier,
|
||||
description,
|
||||
commands,
|
||||
scope,
|
||||
platforms
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PermissionSet {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let identifier = str_lit(&self.identifier);
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
let description = quote! { "".to_string() };
|
||||
let permissions = vec_lit(&self.permissions, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::PermissionSet,
|
||||
identifier,
|
||||
description,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
683
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/resolved.rs
vendored
Normal file
683
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/resolved.rs
vendored
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Resolved ACL for runtime usage.
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
use crate::platform::Target;
|
||||
|
||||
use super::{
|
||||
APP_ACL_KEY, Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes,
|
||||
Value,
|
||||
capability::{Capability, PermissionEntry},
|
||||
has_app_manifest,
|
||||
manifest::Manifest,
|
||||
};
|
||||
|
||||
/// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
|
||||
pub type ScopeKey = u64;
|
||||
|
||||
/// Metadata for what referenced a [`ResolvedCommand`].
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub struct ResolvedCommandReference {
|
||||
/// Identifier of the capability.
|
||||
pub capability: String,
|
||||
/// Identifier of the permission.
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
/// A resolved command permission.
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub struct ResolvedCommand {
|
||||
/// The execution context of this command.
|
||||
pub context: ExecutionContext,
|
||||
/// The capability/permission that referenced this command.
|
||||
#[cfg(debug_assertions)]
|
||||
pub referenced_by: ResolvedCommandReference,
|
||||
/// The list of window label patterns that was resolved for this command.
|
||||
pub windows: Vec<glob::Pattern>,
|
||||
/// The list of webview label patterns that was resolved for this command.
|
||||
pub webviews: Vec<glob::Pattern>,
|
||||
/// The reference of the scope that is associated with this command. See [`Resolved#structfield.command_scopes`].
|
||||
pub scope_id: Option<ScopeKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ResolvedCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ResolvedCommand")
|
||||
.field("context", &self.context)
|
||||
.field("windows", &self.windows)
|
||||
.field("webviews", &self.webviews)
|
||||
.field("scope_id", &self.scope_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A resolved scope. Merges all scopes defined for a single command.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ResolvedScope {
|
||||
/// Allows something on the command.
|
||||
pub allow: Vec<Value>,
|
||||
/// Denies something on the command.
|
||||
pub deny: Vec<Value>,
|
||||
}
|
||||
|
||||
/// Resolved access control list.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Resolved {
|
||||
/// If we should check the ACL for the app commands
|
||||
pub has_app_acl: bool,
|
||||
/// The commands that are allowed. Map each command with its context to a [`ResolvedCommand`].
|
||||
pub allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
|
||||
/// The commands that are denied. Map each command with its context to a [`ResolvedCommand`].
|
||||
pub denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
|
||||
/// The store of scopes referenced by a [`ResolvedCommand`].
|
||||
pub command_scope: BTreeMap<ScopeKey, ResolvedScope>,
|
||||
/// The global scope.
|
||||
pub global_scope: BTreeMap<String, ResolvedScope>,
|
||||
}
|
||||
|
||||
impl Resolved {
|
||||
/// Resolves the ACL for the given plugin permissions and app capabilities.
|
||||
pub fn resolve(
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
mut capabilities: BTreeMap<String, Capability>,
|
||||
target: Target,
|
||||
) -> Result<Self, Error> {
|
||||
let mut allowed_commands = BTreeMap::new();
|
||||
let mut denied_commands = BTreeMap::new();
|
||||
|
||||
let mut current_scope_id = 0;
|
||||
let mut command_scope = BTreeMap::new();
|
||||
let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
|
||||
|
||||
// resolve commands
|
||||
for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
|
||||
with_resolved_permissions(
|
||||
capability,
|
||||
acl,
|
||||
target,
|
||||
|ResolvedPermission {
|
||||
key,
|
||||
commands,
|
||||
scope,
|
||||
#[cfg_attr(not(debug_assertions), allow(unused))]
|
||||
permission_name,
|
||||
}| {
|
||||
if commands.allow.is_empty() && commands.deny.is_empty() {
|
||||
// global scope
|
||||
global_scope.entry(key.to_string()).or_default().push(scope);
|
||||
} else {
|
||||
let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
|
||||
current_scope_id += 1;
|
||||
command_scope.insert(
|
||||
current_scope_id,
|
||||
ResolvedScope {
|
||||
allow: scope.allow.unwrap_or_default(),
|
||||
deny: scope.deny.unwrap_or_default(),
|
||||
},
|
||||
);
|
||||
Some(current_scope_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for allowed_command in &commands.allow {
|
||||
resolve_command(
|
||||
&mut allowed_commands,
|
||||
if key == APP_ACL_KEY {
|
||||
allowed_command.to_string()
|
||||
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
|
||||
format!("plugin:{core_plugin_name}|{allowed_command}")
|
||||
} else {
|
||||
format!("plugin:{key}|{allowed_command}")
|
||||
},
|
||||
capability,
|
||||
scope_id,
|
||||
#[cfg(debug_assertions)]
|
||||
permission_name.to_string(),
|
||||
)?;
|
||||
}
|
||||
|
||||
for denied_command in &commands.deny {
|
||||
resolve_command(
|
||||
&mut denied_commands,
|
||||
if key == APP_ACL_KEY {
|
||||
denied_command.to_string()
|
||||
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
|
||||
format!("plugin:{core_plugin_name}|{denied_command}")
|
||||
} else {
|
||||
format!("plugin:{key}|{denied_command}")
|
||||
},
|
||||
capability,
|
||||
scope_id,
|
||||
#[cfg(debug_assertions)]
|
||||
permission_name.to_string(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
let global_scope = global_scope
|
||||
.into_iter()
|
||||
.map(|(key, scopes)| {
|
||||
let mut resolved_scope = ResolvedScope {
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
};
|
||||
for scope in scopes {
|
||||
if let Some(allow) = scope.allow {
|
||||
resolved_scope.allow.extend(allow);
|
||||
}
|
||||
if let Some(deny) = scope.deny {
|
||||
resolved_scope.deny.extend(deny);
|
||||
}
|
||||
}
|
||||
(key, resolved_scope)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resolved = Self {
|
||||
has_app_acl: has_app_manifest(acl),
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope,
|
||||
global_scope,
|
||||
};
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error> {
|
||||
raw.sort();
|
||||
|
||||
let mut patterns = Vec::new();
|
||||
for pattern in raw {
|
||||
patterns.push(glob::Pattern::new(&pattern)?);
|
||||
}
|
||||
|
||||
Ok(patterns)
|
||||
}
|
||||
|
||||
fn resolve_command(
|
||||
commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
|
||||
command: String,
|
||||
capability: &Capability,
|
||||
scope_id: Option<ScopeKey>,
|
||||
#[cfg(debug_assertions)] referenced_by_permission_identifier: String,
|
||||
) -> Result<(), Error> {
|
||||
let mut contexts = Vec::new();
|
||||
if capability.local {
|
||||
contexts.push(ExecutionContext::Local);
|
||||
}
|
||||
if let Some(remote) = &capability.remote {
|
||||
contexts.extend(remote.urls.iter().map(|url| {
|
||||
ExecutionContext::Remote {
|
||||
url: url
|
||||
.parse()
|
||||
.unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for context in contexts {
|
||||
let resolved_list = commands.entry(command.clone()).or_default();
|
||||
|
||||
resolved_list.push(ResolvedCommand {
|
||||
context,
|
||||
#[cfg(debug_assertions)]
|
||||
referenced_by: ResolvedCommandReference {
|
||||
capability: capability.identifier.clone(),
|
||||
permission: referenced_by_permission_identifier.clone(),
|
||||
},
|
||||
windows: parse_glob_patterns(capability.windows.clone())?,
|
||||
webviews: parse_glob_patterns(capability.webviews.clone())?,
|
||||
scope_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ResolvedPermission<'a> {
|
||||
key: &'a str,
|
||||
permission_name: &'a str,
|
||||
commands: Commands,
|
||||
scope: Scopes,
|
||||
}
|
||||
|
||||
/// Iterate over permissions in a capability, resolving permission sets if necessary
|
||||
/// to produce a [`ResolvedPermission`] and calling the provided callback with it.
|
||||
fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
|
||||
capability: &Capability,
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
target: Target,
|
||||
mut f: F,
|
||||
) -> Result<(), Error> {
|
||||
for permission_entry in &capability.permissions {
|
||||
let permission_id = permission_entry.identifier();
|
||||
|
||||
let permissions = get_permissions(permission_id, acl)?
|
||||
.into_iter()
|
||||
.filter(|p| p.permission.is_active(&target));
|
||||
|
||||
for TraversedPermission {
|
||||
key,
|
||||
permission_name,
|
||||
permission,
|
||||
} in permissions
|
||||
{
|
||||
let mut resolved_scope = Scopes::default();
|
||||
let mut commands = Commands::default();
|
||||
|
||||
if let PermissionEntry::ExtendedPermission {
|
||||
identifier: _,
|
||||
scope,
|
||||
} = permission_entry
|
||||
{
|
||||
if let Some(allow) = scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(allow) = permission.scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = permission.scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
|
||||
commands.allow.extend(permission.commands.allow.clone());
|
||||
commands.deny.extend(permission.commands.deny.clone());
|
||||
|
||||
f(ResolvedPermission {
|
||||
key: &key,
|
||||
permission_name: &permission_name,
|
||||
commands,
|
||||
scope: resolved_scope,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Traversed permission
|
||||
#[derive(Debug)]
|
||||
pub struct TraversedPermission<'a> {
|
||||
/// Plugin name without the tauri-plugin- prefix
|
||||
pub key: String,
|
||||
/// Permission's name
|
||||
pub permission_name: String,
|
||||
/// Permission details
|
||||
pub permission: &'a Permission,
|
||||
}
|
||||
|
||||
/// Expand a permissions id based on the ACL to get the associated permissions (e.g. expand some-plugin:default)
|
||||
pub fn get_permissions<'a>(
|
||||
permission_id: &Identifier,
|
||||
acl: &'a BTreeMap<String, Manifest>,
|
||||
) -> Result<Vec<TraversedPermission<'a>>, Error> {
|
||||
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
|
||||
let permission_name = permission_id.get_base();
|
||||
|
||||
let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
|
||||
key: display_perm_key(key).to_string(),
|
||||
available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
|
||||
})?;
|
||||
|
||||
if permission_name == "default" {
|
||||
manifest
|
||||
.default_permission
|
||||
.as_ref()
|
||||
.map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
|
||||
.unwrap_or_else(|| Ok(Default::default()))
|
||||
} else if let Some(set) = manifest.permission_sets.get(permission_name) {
|
||||
get_permission_set_permissions(permission_id, acl, manifest, set)
|
||||
} else if let Some(permission) = manifest.permissions.get(permission_name) {
|
||||
Ok(vec![TraversedPermission {
|
||||
key: key.to_string(),
|
||||
permission_name: permission_name.to_string(),
|
||||
permission,
|
||||
}])
|
||||
} else {
|
||||
Err(Error::UnknownPermission {
|
||||
key: display_perm_key(key).to_string(),
|
||||
permission: permission_name.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// get the permissions from a permission set
|
||||
fn get_permission_set_permissions<'a>(
|
||||
permission_id: &Identifier,
|
||||
acl: &'a BTreeMap<String, Manifest>,
|
||||
manifest: &'a Manifest,
|
||||
set: &'a PermissionSet,
|
||||
) -> Result<Vec<TraversedPermission<'a>>, Error> {
|
||||
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
for perm in &set.permissions {
|
||||
// a set could include permissions from other plugins
|
||||
// for example `dialog:default`, could include `fs:default`
|
||||
// in this case `perm = "fs:default"` which is not a permission
|
||||
// in the dialog manifest so we check if `perm` still have a prefix (i.e `fs:`)
|
||||
// and if so, we resolve this prefix from `acl` first before proceeding
|
||||
let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
|
||||
let (manifest, permission_id, key, permission_name) =
|
||||
if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
|
||||
(manifest, &id, new_key, id.get_base())
|
||||
} else {
|
||||
(manifest, permission_id, key, perm.as_str())
|
||||
};
|
||||
|
||||
if permission_name == "default" {
|
||||
permissions.extend(
|
||||
manifest
|
||||
.default_permission
|
||||
.as_ref()
|
||||
.map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
} else if let Some(permission) = manifest.permissions.get(permission_name) {
|
||||
permissions.push(TraversedPermission {
|
||||
key: key.to_string(),
|
||||
permission_name: permission_name.to_string(),
|
||||
permission,
|
||||
});
|
||||
} else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
|
||||
permissions.extend(get_permission_set_permissions(
|
||||
permission_id,
|
||||
acl,
|
||||
manifest,
|
||||
permission_set,
|
||||
)?);
|
||||
} else {
|
||||
return Err(Error::SetPermissionNotFound {
|
||||
permission: permission_name.to_string(),
|
||||
set: set.identifier.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(permissions)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn display_perm_key(prefix: &str) -> &str {
|
||||
if prefix == APP_ACL_KEY {
|
||||
"app manifest"
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
use std::convert::identity;
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl ToTokens for ResolvedCommandReference {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let capability = str_lit(&self.capability);
|
||||
let permission = str_lit(&self.permission);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommandReference,
|
||||
capability,
|
||||
permission
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ResolvedCommand {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
#[cfg(debug_assertions)]
|
||||
let referenced_by = &self.referenced_by;
|
||||
|
||||
let context = &self.context;
|
||||
|
||||
let windows = vec_lit(&self.windows, |window| {
|
||||
let w = window.as_str();
|
||||
quote!(#w.parse().unwrap())
|
||||
});
|
||||
let webviews = vec_lit(&self.webviews, |window| {
|
||||
let w = window.as_str();
|
||||
quote!(#w.parse().unwrap())
|
||||
});
|
||||
let scope_id = opt_lit(self.scope_id.as_ref());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommand,
|
||||
context,
|
||||
referenced_by,
|
||||
windows,
|
||||
webviews,
|
||||
scope_id
|
||||
)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommand,
|
||||
context,
|
||||
windows,
|
||||
webviews,
|
||||
scope_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ResolvedScope {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = vec_lit(&self.allow, identity);
|
||||
let deny = vec_lit(&self.deny, identity);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedScope,
|
||||
allow,
|
||||
deny
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Resolved {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let has_app_acl = self.has_app_acl;
|
||||
|
||||
let allowed_commands = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.allowed_commands,
|
||||
str_lit,
|
||||
|v| vec_lit(v, identity),
|
||||
);
|
||||
|
||||
let denied_commands = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.denied_commands,
|
||||
str_lit,
|
||||
|v| vec_lit(v, identity),
|
||||
);
|
||||
|
||||
let command_scope = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.command_scope,
|
||||
identity,
|
||||
identity,
|
||||
);
|
||||
|
||||
let global_scope = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.global_scope,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::Resolved,
|
||||
has_app_acl,
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope,
|
||||
global_scope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::{Identifier, Manifest, Permission, PermissionSet, get_permissions};
|
||||
|
||||
fn manifest<const P: usize, const S: usize>(
|
||||
name: &str,
|
||||
permissions: [&str; P],
|
||||
default_set: Option<&[&str]>,
|
||||
sets: [(&str, &[&str]); S],
|
||||
) -> (String, Manifest) {
|
||||
(
|
||||
name.to_string(),
|
||||
Manifest {
|
||||
default_permission: default_set.map(|perms| PermissionSet {
|
||||
identifier: "default".to_string(),
|
||||
description: "default set".to_string(),
|
||||
permissions: perms.iter().map(|s| s.to_string()).collect(),
|
||||
}),
|
||||
permissions: permissions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
p.to_string(),
|
||||
Permission {
|
||||
identifier: p.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
permission_sets: sets
|
||||
.iter()
|
||||
.map(|(s, perms)| {
|
||||
(
|
||||
s.to_string(),
|
||||
PermissionSet {
|
||||
identifier: s.to_string(),
|
||||
description: format!("{s} set"),
|
||||
permissions: perms.iter().map(|s| s.to_string()).collect(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn id(id: &str) -> Identifier {
|
||||
Identifier::try_from(id.to_string()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_permissions_from_other_plugins() {
|
||||
let acl = [
|
||||
manifest(
|
||||
"fs",
|
||||
["read", "write", "rm", "exist"],
|
||||
Some(&["read", "exist"]),
|
||||
[],
|
||||
),
|
||||
manifest(
|
||||
"http",
|
||||
["fetch", "fetch-cancel"],
|
||||
None,
|
||||
[("fetch-with-cancel", &["fetch", "fetch-cancel"])],
|
||||
),
|
||||
manifest(
|
||||
"dialog",
|
||||
["open", "save"],
|
||||
None,
|
||||
[(
|
||||
"extra",
|
||||
&[
|
||||
"save",
|
||||
"fs:default",
|
||||
"fs:write",
|
||||
"http:default",
|
||||
"http:fetch-with-cancel",
|
||||
],
|
||||
)],
|
||||
),
|
||||
]
|
||||
.into();
|
||||
|
||||
let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 2);
|
||||
assert_eq!(permissions[0].key, "fs");
|
||||
assert_eq!(permissions[0].permission_name, "read");
|
||||
assert_eq!(permissions[1].key, "fs");
|
||||
assert_eq!(permissions[1].permission_name, "exist");
|
||||
|
||||
let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 1);
|
||||
assert_eq!(permissions[0].key, "fs");
|
||||
assert_eq!(permissions[0].permission_name, "rm");
|
||||
|
||||
let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 2);
|
||||
assert_eq!(permissions[0].key, "http");
|
||||
assert_eq!(permissions[0].permission_name, "fetch");
|
||||
assert_eq!(permissions[1].key, "http");
|
||||
assert_eq!(permissions[1].permission_name, "fetch-cancel");
|
||||
|
||||
let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 6);
|
||||
assert_eq!(permissions[0].key, "dialog");
|
||||
assert_eq!(permissions[0].permission_name, "save");
|
||||
assert_eq!(permissions[1].key, "fs");
|
||||
assert_eq!(permissions[1].permission_name, "read");
|
||||
assert_eq!(permissions[2].key, "fs");
|
||||
assert_eq!(permissions[2].permission_name, "exist");
|
||||
assert_eq!(permissions[3].key, "fs");
|
||||
assert_eq!(permissions[3].permission_name, "write");
|
||||
assert_eq!(permissions[4].key, "http");
|
||||
assert_eq!(permissions[4].permission_name, "fetch");
|
||||
assert_eq!(permissions[5].key, "http");
|
||||
assert_eq!(permissions[5].permission_name, "fetch-cancel");
|
||||
}
|
||||
}
|
||||
445
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/schema.rs
vendored
Normal file
445
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/schema.rs
vendored
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Schema generation for ACL items.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, btree_map::Values},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
slice::Iter,
|
||||
};
|
||||
|
||||
use schemars::Schema;
|
||||
|
||||
use super::{Error, PERMISSION_SCHEMAS_FOLDER_NAME};
|
||||
use crate::{platform::Target, write_if_changed};
|
||||
|
||||
use super::{
|
||||
PERMISSION_SCHEMA_FILE_NAME, Permission, PermissionSet,
|
||||
capability::CapabilityFile,
|
||||
manifest::{Manifest, PermissionFile},
|
||||
};
|
||||
|
||||
/// Capability schema file name.
|
||||
pub const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
|
||||
/// Path of the folder where schemas are saved.
|
||||
pub const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
|
||||
|
||||
// TODO: once MSRV is high enough, remove generic and use impl <trait>
|
||||
// see https://github.com/tauri-apps/tauri/commit/b5561d74aee431f93c0c5b0fa6784fc0a956effe#diff-7c31d393f83cae149122e74ad44ac98e7d70ffb45c9e5b0a94ec52881b6f1cebR30-R42
|
||||
/// Permission schema generator trait
|
||||
pub trait PermissionSchemaGenerator<
|
||||
'a,
|
||||
Ps: Iterator<Item = &'a PermissionSet>,
|
||||
P: Iterator<Item = &'a Permission>,
|
||||
>
|
||||
{
|
||||
/// Whether has a default permission set or not.
|
||||
fn has_default_permission_set(&self) -> bool;
|
||||
|
||||
/// Default permission set description if any.
|
||||
fn default_set_description(&self) -> Option<&str>;
|
||||
|
||||
/// Default permission set's permissions if any.
|
||||
fn default_set_permissions(&self) -> Option<&Vec<String>>;
|
||||
|
||||
/// Permissions sets to generate schema for.
|
||||
fn permission_sets(&'a self) -> Ps;
|
||||
|
||||
/// Permissions to generate schema for.
|
||||
fn permissions(&'a self) -> P;
|
||||
|
||||
/// A utility function to generate a schema for a permission identifier
|
||||
fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema {
|
||||
let command_name = match name {
|
||||
Some(name) if name == super::APP_ACL_KEY => id.to_string(),
|
||||
Some(name) => format!("{name}:{id}"),
|
||||
_ => id.to_string(),
|
||||
};
|
||||
|
||||
let mut schema = schemars::json_schema!({
|
||||
"type": "string",
|
||||
"const": command_name
|
||||
});
|
||||
|
||||
if let Some(description) = description {
|
||||
schema.insert(
|
||||
"description".to_string(),
|
||||
serde_json::Value::String(description.to_string()),
|
||||
);
|
||||
// Non-standard, used by vscode for rich hover tooltips
|
||||
schema.insert(
|
||||
"markdownDescription".to_string(),
|
||||
serde_json::Value::String(description.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
schema
|
||||
}
|
||||
|
||||
/// Generate schemas for all possible permissions.
|
||||
fn gen_possible_permission_schemas(&'a self, name: Option<&str>) -> Vec<Schema> {
|
||||
let mut permission_schemas = Vec::new();
|
||||
|
||||
// schema for default set
|
||||
if self.has_default_permission_set() {
|
||||
let description = self.default_set_description().unwrap_or_default();
|
||||
let description = if let Some(permissions) = self.default_set_permissions() {
|
||||
add_permissions_to_description(description, permissions, true)
|
||||
} else {
|
||||
description.to_string()
|
||||
};
|
||||
if !description.is_empty() {
|
||||
let default = Self::perm_id_schema(name, "default", Some(&description));
|
||||
permission_schemas.push(default);
|
||||
}
|
||||
}
|
||||
|
||||
// schema for each permission set
|
||||
for set in self.permission_sets() {
|
||||
let description = add_permissions_to_description(&set.description, &set.permissions, false);
|
||||
let schema = Self::perm_id_schema(name, &set.identifier, Some(&description));
|
||||
permission_schemas.push(schema);
|
||||
}
|
||||
|
||||
// schema for each permission
|
||||
for perm in self.permissions() {
|
||||
let schema = Self::perm_id_schema(name, &perm.identifier, perm.description.as_deref());
|
||||
permission_schemas.push(schema);
|
||||
}
|
||||
|
||||
permission_schemas
|
||||
}
|
||||
}
|
||||
|
||||
fn add_permissions_to_description(
|
||||
description: &str,
|
||||
permissions: &[String],
|
||||
is_default: bool,
|
||||
) -> String {
|
||||
if permissions.is_empty() {
|
||||
return description.to_string();
|
||||
}
|
||||
let permissions_list = permissions
|
||||
.iter()
|
||||
.map(|permission| format!("- `{permission}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let default_permission_set = if is_default {
|
||||
"default permission set"
|
||||
} else {
|
||||
"permission set"
|
||||
};
|
||||
format!("{description}\n#### This {default_permission_set} includes:\n\n{permissions_list}")
|
||||
}
|
||||
|
||||
impl<'a>
|
||||
PermissionSchemaGenerator<
|
||||
'a,
|
||||
Values<'a, std::string::String, PermissionSet>,
|
||||
Values<'a, std::string::String, Permission>,
|
||||
> for Manifest
|
||||
{
|
||||
fn has_default_permission_set(&self) -> bool {
|
||||
self.default_permission.is_some()
|
||||
}
|
||||
|
||||
fn default_set_description(&self) -> Option<&str> {
|
||||
self
|
||||
.default_permission
|
||||
.as_ref()
|
||||
.map(|d| d.description.as_str())
|
||||
}
|
||||
|
||||
fn default_set_permissions(&self) -> Option<&Vec<String>> {
|
||||
self.default_permission.as_ref().map(|d| &d.permissions)
|
||||
}
|
||||
|
||||
fn permission_sets(&'a self) -> Values<'a, std::string::String, PermissionSet> {
|
||||
self.permission_sets.values()
|
||||
}
|
||||
|
||||
fn permissions(&'a self) -> Values<'a, std::string::String, Permission> {
|
||||
self.permissions.values()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>>
|
||||
for PermissionFile
|
||||
{
|
||||
fn has_default_permission_set(&self) -> bool {
|
||||
self.default.is_some()
|
||||
}
|
||||
|
||||
fn default_set_description(&self) -> Option<&str> {
|
||||
self.default.as_ref().and_then(|d| d.description.as_deref())
|
||||
}
|
||||
|
||||
fn default_set_permissions(&self) -> Option<&Vec<String>> {
|
||||
self.default.as_ref().map(|d| &d.permissions)
|
||||
}
|
||||
|
||||
fn permission_sets(&'a self) -> Iter<'a, PermissionSet> {
|
||||
self.set.iter()
|
||||
}
|
||||
|
||||
fn permissions(&'a self) -> Iter<'a, Permission> {
|
||||
self.permission.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect and include all possible identifiers in `Identifier` definition in the schema
|
||||
fn extend_identifier_schema(schema: &mut Schema, acl: &BTreeMap<String, Manifest>) {
|
||||
let permission_schemas: Vec<serde_json::Value> = acl
|
||||
.iter()
|
||||
.flat_map(|(name, manifest)| manifest.gen_possible_permission_schemas(Some(name)))
|
||||
.map(serde_json::Value::from)
|
||||
.collect();
|
||||
|
||||
if let Some(identifier_schema) = schema
|
||||
.pointer_mut("/$defs/Identifier")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
identifier_schema.insert(
|
||||
"oneOf".to_string(),
|
||||
serde_json::Value::Array(permission_schemas),
|
||||
);
|
||||
identifier_schema.remove("properties");
|
||||
identifier_schema.remove("type");
|
||||
identifier_schema.insert(
|
||||
"description".to_string(),
|
||||
serde_json::Value::String("Permission identifier".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect permission schemas and its associated scope schema and schema definitions from plugins
|
||||
/// and replace `PermissionEntry` extend object syntax with a new schema that does conditional
|
||||
/// checks to serve the relevant scope schema for the right permissions schema, in a nutshell, it
|
||||
/// will look something like this:
|
||||
/// ```text
|
||||
/// PermissionEntry {
|
||||
/// anyOf {
|
||||
/// String, // default string syntax
|
||||
/// Object { // extended object syntax
|
||||
/// allOf { // JSON allOf is used but actually means anyOf
|
||||
/// {
|
||||
/// "if": "identifier" property anyOf "fs" plugin permission,
|
||||
/// "then": add "allow" and "deny" properties that match "fs" plugin scope schema
|
||||
/// },
|
||||
/// {
|
||||
/// "if": "identifier" property anyOf "http" plugin permission,
|
||||
/// "then": add "allow" and "deny" properties that match "http" plugin scope schema
|
||||
/// },
|
||||
/// ...etc,
|
||||
/// {
|
||||
/// No "if" or "then", just "allow" and "deny" properties with default "#/defs/Value"
|
||||
/// },
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn extend_permission_entry_schema(root_schema: &mut Schema, acl: &BTreeMap<String, Manifest>) {
|
||||
const IDENTIFIER: &str = "identifier";
|
||||
const ALLOW: &str = "allow";
|
||||
const DENY: &str = "deny";
|
||||
|
||||
let mut collected_defs: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
|
||||
|
||||
// Scope the mutable borrow of root_schema
|
||||
{
|
||||
let defs = match root_schema.get_mut("$defs").and_then(|v| v.as_object_mut()) {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let perm_entry = match defs
|
||||
.get_mut("PermissionEntry")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let any_of = match perm_entry.get_mut("anyOf").and_then(|v| v.as_array_mut()) {
|
||||
Some(a) => a,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let extend_perm_entry = match any_of.last_mut().and_then(|v| v.as_object_mut()) {
|
||||
Some(e) => e,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Remove default properties and save to be added later as a fallback
|
||||
let default_properties = extend_perm_entry
|
||||
.remove("properties")
|
||||
.and_then(|v| match v {
|
||||
serde_json::Value::Object(m) => Some(m),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let default_identifier = default_properties.get(IDENTIFIER).cloned().unwrap();
|
||||
|
||||
let mut all_of: Vec<serde_json::Value> = vec![];
|
||||
|
||||
let schemas = acl.iter().filter_map(|(name, manifest)| {
|
||||
manifest
|
||||
.global_scope_schema()
|
||||
.unwrap_or_else(|e| panic!("invalid JSON schema for plugin {name}: {e}"))
|
||||
.map(|s| (s, manifest.gen_possible_permission_schemas(Some(name))))
|
||||
});
|
||||
|
||||
for ((scope_schema, defs), acl_perm_schema) in schemas {
|
||||
let perm_schema_values: Vec<serde_json::Value> = acl_perm_schema
|
||||
.into_iter()
|
||||
.map(serde_json::Value::from)
|
||||
.collect();
|
||||
|
||||
let scope_value = serde_json::Value::from(scope_schema);
|
||||
|
||||
let obj = serde_json::json!({
|
||||
"properties": {
|
||||
IDENTIFIER: default_identifier.clone()
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
IDENTIFIER: { "anyOf": perm_schema_values }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
ALLOW: scope_value.clone(),
|
||||
DENY: scope_value,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
all_of.push(obj);
|
||||
collected_defs.extend(defs);
|
||||
}
|
||||
|
||||
// Add back default properties as a fallback
|
||||
all_of.push(serde_json::json!({
|
||||
"properties": serde_json::Value::Object(default_properties)
|
||||
}));
|
||||
|
||||
// Replace extended PermissionEntry with the new schema
|
||||
extend_perm_entry.insert("allOf".to_string(), serde_json::Value::Array(all_of));
|
||||
}
|
||||
|
||||
// Extend root schema with definitions collected from plugins
|
||||
if !collected_defs.is_empty() {
|
||||
let root_defs = root_schema
|
||||
.ensure_object()
|
||||
.entry("$defs")
|
||||
.or_insert(serde_json::Value::Object(serde_json::Map::new()))
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
|
||||
root_defs.extend(collected_defs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate schema for CapabilityFile with all possible plugins permissions
|
||||
pub fn generate_capability_schema(
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
target: Target,
|
||||
) -> crate::Result<()> {
|
||||
let mut schema = schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
|
||||
.into_root_schema_for::<CapabilityFile>();
|
||||
|
||||
extend_identifier_schema(&mut schema, acl);
|
||||
extend_permission_entry_schema(&mut schema, acl);
|
||||
|
||||
let schema_str = serde_json::to_string_pretty(&schema).unwrap();
|
||||
|
||||
let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
|
||||
fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
|
||||
if schema_str != fs::read_to_string(&schema_path).unwrap_or_default() {
|
||||
fs::write(&schema_path, schema_str)?;
|
||||
|
||||
fs::copy(
|
||||
schema_path,
|
||||
out_dir.join(format!(
|
||||
"{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
|
||||
if target.is_desktop() {
|
||||
"desktop"
|
||||
} else {
|
||||
"mobile"
|
||||
}
|
||||
)),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extend schema with collected permissions from the passed [`PermissionFile`]s.
|
||||
fn extend_permission_file_schema(schema: &mut Schema, permissions: &[PermissionFile]) {
|
||||
// Collect possible permissions
|
||||
let permission_schemas: Vec<serde_json::Value> = permissions
|
||||
.iter()
|
||||
.flat_map(|p| p.gen_possible_permission_schemas(None))
|
||||
.map(serde_json::Value::from)
|
||||
.collect();
|
||||
|
||||
// Update the permissions property to reference PermissionKind
|
||||
let updated = if let Some(permissions_obj) = schema
|
||||
.pointer_mut("/$defs/PermissionSet/properties/permissions")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
permissions_obj.insert(
|
||||
"items".to_string(),
|
||||
serde_json::json!({ "$ref": "#/$defs/PermissionKind" }),
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Add the new PermissionKind definition
|
||||
if updated {
|
||||
let defs = schema
|
||||
.ensure_object()
|
||||
.entry("$defs")
|
||||
.or_insert(serde_json::Value::Object(serde_json::Map::new()))
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
|
||||
defs.insert(
|
||||
"PermissionKind".into(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"oneOf": permission_schemas,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate and write a schema based on the format of a [`PermissionFile`].
|
||||
pub fn generate_permissions_schema<P: AsRef<Path>>(
|
||||
permissions: &[PermissionFile],
|
||||
out_dir: P,
|
||||
) -> Result<(), Error> {
|
||||
let mut schema = schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
|
||||
.into_root_schema_for::<PermissionFile>();
|
||||
|
||||
extend_permission_file_schema(&mut schema, permissions);
|
||||
|
||||
let schema_str = serde_json::to_string_pretty(&schema)?;
|
||||
|
||||
let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME);
|
||||
fs::create_dir_all(&out_dir).map_err(|e| Error::CreateDir(e, out_dir.clone()))?;
|
||||
|
||||
let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME);
|
||||
write_if_changed(&schema_path, schema_str).map_err(|e| Error::WriteFile(e, schema_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
201
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/value.rs
vendored
Normal file
201
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/value.rs
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! A [`Value`] that is used instead of [`toml::Value`] or [`serde_json::Value`]
|
||||
//! to support both formats.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A valid ACL number.
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Copy, Clone, PartialOrd)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Number {
|
||||
/// Represents an [`i64`].
|
||||
Int(i64),
|
||||
|
||||
/// Represents a [`f64`].
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
impl From<i64> for Number {
|
||||
#[inline(always)]
|
||||
fn from(value: i64) -> Self {
|
||||
Self::Int(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Number {
|
||||
#[inline(always)]
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// All supported ACL values.
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, PartialOrd)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Value {
|
||||
/// Represents a null JSON value.
|
||||
Null,
|
||||
|
||||
/// Represents a [`bool`].
|
||||
Bool(bool),
|
||||
|
||||
/// Represents a valid ACL [`Number`].
|
||||
Number(Number),
|
||||
|
||||
/// Represents a [`String`].
|
||||
String(String),
|
||||
|
||||
/// Represents a list of other [`Value`]s.
|
||||
List(Vec<Value>),
|
||||
|
||||
/// Represents a map of [`String`] keys to [`Value`]s.
|
||||
Map(BTreeMap<String, Value>),
|
||||
}
|
||||
|
||||
impl From<Value> for serde_json::Value {
|
||||
fn from(value: Value) -> Self {
|
||||
match value {
|
||||
Value::Null => serde_json::Value::Null,
|
||||
Value::Bool(b) => serde_json::Value::Bool(b),
|
||||
Value::Number(Number::Float(f)) => {
|
||||
serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
|
||||
}
|
||||
Value::Number(Number::Int(i)) => serde_json::Value::Number(i.into()),
|
||||
Value::String(s) => serde_json::Value::String(s),
|
||||
Value::List(list) => serde_json::Value::Array(list.into_iter().map(Into::into).collect()),
|
||||
Value::Map(map) => serde_json::Value::Object(
|
||||
map
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.into()))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for Value {
|
||||
fn from(value: serde_json::Value) -> Self {
|
||||
match value {
|
||||
serde_json::Value::Null => Value::Null,
|
||||
serde_json::Value::Bool(b) => Value::Bool(b),
|
||||
serde_json::Value::Number(n) => Value::Number(if let Some(f) = n.as_f64() {
|
||||
Number::Float(f)
|
||||
} else if let Some(n) = n.as_u64() {
|
||||
Number::Int(n as i64)
|
||||
} else if let Some(n) = n.as_i64() {
|
||||
Number::Int(n)
|
||||
} else {
|
||||
Number::Int(0)
|
||||
}),
|
||||
serde_json::Value::String(s) => Value::String(s),
|
||||
serde_json::Value::Array(list) => Value::List(list.into_iter().map(Into::into).collect()),
|
||||
serde_json::Value::Object(map) => Value::Map(
|
||||
map
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.into()))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Number>> From<T> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: T) -> Self {
|
||||
Self::Number(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: String) -> Self {
|
||||
Value::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::Value> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: toml::Value) -> Self {
|
||||
use toml::Value as Toml;
|
||||
|
||||
match value {
|
||||
Toml::String(s) => s.into(),
|
||||
Toml::Integer(i) => i.into(),
|
||||
Toml::Float(f) => f.into(),
|
||||
Toml::Boolean(b) => b.into(),
|
||||
Toml::Datetime(d) => d.to_string().into(),
|
||||
Toml::Array(a) => Value::List(a.into_iter().map(Value::from).collect()),
|
||||
Toml::Table(t) => Value::Map(t.into_iter().map(|(k, v)| (k, v.into())).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use std::convert::identity;
|
||||
|
||||
use crate::tokens::*;
|
||||
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
impl ToTokens for Number {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::Number };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::Int(i) => {
|
||||
quote! { #prefix::Int(#i) }
|
||||
}
|
||||
Self::Float(f) => {
|
||||
quote! { #prefix::Float (#f) }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Value {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::Value };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Value::Null => quote! { #prefix::Null },
|
||||
Value::Bool(bool) => quote! { #prefix::Bool(#bool) },
|
||||
Value::Number(number) => quote! { #prefix::Number(#number) },
|
||||
Value::String(str) => {
|
||||
let s = str_lit(str);
|
||||
quote! { #prefix::String(#s) }
|
||||
}
|
||||
Value::List(vec) => {
|
||||
let items = vec_lit(vec, identity);
|
||||
quote! { #prefix::List(#items) }
|
||||
}
|
||||
Value::Map(map) => {
|
||||
let map = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
map,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
quote! { #prefix::Map(#map) }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
212
apps/tauri/src-tauri/vendor/tauri-utils/src/assets.rs
vendored
Normal file
212
apps/tauri/src-tauri/vendor/tauri-utils/src/assets.rs
vendored
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The Assets module allows you to read files that have been bundled by tauri
|
||||
//! during both compile time and runtime.
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use phf;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Component, Path},
|
||||
};
|
||||
|
||||
/// The token used for script nonces.
|
||||
pub const SCRIPT_NONCE_TOKEN: &str = "__TAURI_SCRIPT_NONCE__";
|
||||
/// The token used for style nonces.
|
||||
pub const STYLE_NONCE_TOKEN: &str = "__TAURI_STYLE_NONCE__";
|
||||
|
||||
/// Assets iterator.
|
||||
pub type AssetsIter<'a> = dyn Iterator<Item = (Cow<'a, str>, Cow<'a, [u8]>)> + 'a;
|
||||
|
||||
/// Represent an asset file path in a normalized way.
|
||||
///
|
||||
/// The following rules are enforced and added if needed:
|
||||
/// * Unix path component separators
|
||||
/// * Has a root directory
|
||||
/// * No trailing slash - directories are not included in assets
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct AssetKey(String);
|
||||
|
||||
impl From<AssetKey> for String {
|
||||
fn from(key: AssetKey) -> Self {
|
||||
key.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AssetKey {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> From<P> for AssetKey {
|
||||
fn from(path: P) -> Self {
|
||||
// TODO: change this to utilize `Cow` to prevent allocating an intermediate `PathBuf` when not necessary
|
||||
let path = path.as_ref().to_owned();
|
||||
|
||||
// add in root to mimic how it is used from a server url
|
||||
let path = if path.has_root() {
|
||||
path
|
||||
} else {
|
||||
Path::new(&Component::RootDir).join(path)
|
||||
};
|
||||
|
||||
let buf = if cfg!(windows) {
|
||||
let mut buf = String::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::RootDir => buf.push('/'),
|
||||
Component::CurDir => buf.push_str("./"),
|
||||
Component::ParentDir => buf.push_str("../"),
|
||||
Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
|
||||
Component::Normal(s) => {
|
||||
buf.push_str(&s.to_string_lossy());
|
||||
buf.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove the last slash
|
||||
if buf != "/" {
|
||||
buf.pop();
|
||||
}
|
||||
|
||||
buf
|
||||
} else {
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
AssetKey(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Content-Security-Policy hash value for a specific directive.
|
||||
/// For more information see [the MDN page](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives).
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CspHash<'a> {
|
||||
/// The `script-src` directive.
|
||||
Script(&'a str),
|
||||
|
||||
/// The `style-src` directive.
|
||||
Style(&'a str),
|
||||
}
|
||||
|
||||
impl CspHash<'_> {
|
||||
/// The Content-Security-Policy directive this hash applies to.
|
||||
pub fn directive(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Script(_) => "script-src",
|
||||
Self::Style(_) => "style-src",
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the Content-Security-Policy hash.
|
||||
pub fn hash(&self) -> &str {
|
||||
match self {
|
||||
Self::Script(hash) => hash,
|
||||
Self::Style(hash) => hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
|
||||
pub struct EmbeddedAssets {
|
||||
assets: phf::Map<&'static str, &'static [u8]>,
|
||||
// Hashes that must be injected to the CSP of every HTML file.
|
||||
global_hashes: &'static [CspHash<'static>],
|
||||
// Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
|
||||
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
|
||||
}
|
||||
|
||||
/// Temporary struct that overrides the Debug formatting for the `assets` field.
|
||||
///
|
||||
/// It reduces the output size compared to the default, as that would format the binary
|
||||
/// data as a slice of numbers like `[65, 66, 67]` for "ABC". This instead shows the length
|
||||
/// of the slice.
|
||||
///
|
||||
/// For example: `{"/index.html": [u8; 1835], "/index.js": [u8; 212]}`
|
||||
struct DebugAssetMap<'a>(&'a phf::Map<&'static str, &'static [u8]>);
|
||||
|
||||
impl std::fmt::Debug for DebugAssetMap<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
for (k, v) in self.0.entries() {
|
||||
map.key(k);
|
||||
map.value(&format_args!("[u8; {}]", v.len()));
|
||||
}
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EmbeddedAssets {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EmbeddedAssets")
|
||||
.field("assets", &DebugAssetMap(&self.assets))
|
||||
.field("global_hashes", &self.global_hashes)
|
||||
.field("html_hashes", &self.html_hashes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbeddedAssets {
|
||||
/// Creates a new instance from the given asset map and script hash list.
|
||||
pub const fn new(
|
||||
map: phf::Map<&'static str, &'static [u8]>,
|
||||
global_hashes: &'static [CspHash<'static>],
|
||||
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assets: map,
|
||||
global_hashes,
|
||||
html_hashes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an asset by key.
|
||||
#[cfg(feature = "compression")]
|
||||
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
|
||||
let &(mut asdf) = self.assets.get(key.as_ref())?;
|
||||
// with the exception of extremely small files, output should usually be
|
||||
// at least as large as the compressed version.
|
||||
let mut buf = Vec::with_capacity(asdf.len());
|
||||
brotli::BrotliDecompress(&mut asdf, &mut buf).ok()?;
|
||||
Some(Cow::Owned(buf))
|
||||
}
|
||||
|
||||
/// Get an asset by key.
|
||||
#[cfg(not(feature = "compression"))]
|
||||
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
|
||||
Some(Cow::Borrowed(self.assets.get(key.as_ref())?))
|
||||
}
|
||||
|
||||
/// Iterate on the assets.
|
||||
pub fn iter(&self) -> Box<AssetsIter<'_>> {
|
||||
Box::new(
|
||||
self
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|(k, b)| (Cow::Borrowed(*k), Cow::Borrowed(*b))),
|
||||
)
|
||||
}
|
||||
|
||||
/// CSP hashes for the given asset.
|
||||
pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
|
||||
Box::new(
|
||||
self
|
||||
.global_hashes
|
||||
.iter()
|
||||
.chain(
|
||||
self
|
||||
.html_hashes
|
||||
.get(html_path.as_ref())
|
||||
.copied()
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.copied(),
|
||||
)
|
||||
}
|
||||
}
|
||||
171
apps/tauri/src-tauri/vendor/tauri-utils/src/build.rs
vendored
Normal file
171
apps/tauri/src-tauri/vendor/tauri-utils/src/build.rs
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Build script utilities.
|
||||
|
||||
/// Link a Swift library.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn link_apple_library(name: &str, source: impl AsRef<std::path::Path>) {
|
||||
if source.as_ref().join("Package.swift").exists() {
|
||||
link_swift_library(name, source);
|
||||
} else {
|
||||
link_xcode_library(name, source);
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a Swift library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn link_swift_library(name: &str, source: impl AsRef<std::path::Path>) {
|
||||
let source = source.as_ref();
|
||||
|
||||
let sdk_root = std::env::var_os("SDKROOT");
|
||||
unsafe {
|
||||
std::env::remove_var("SDKROOT");
|
||||
}
|
||||
|
||||
swift_rs::SwiftLinker::new(
|
||||
&std::env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| "10.13".into()),
|
||||
)
|
||||
.with_ios(&std::env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "13.0".into()))
|
||||
.with_package(name, source)
|
||||
.link();
|
||||
|
||||
if let Some(root) = sdk_root {
|
||||
unsafe {
|
||||
std::env::set_var("SDKROOT", root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a Xcode library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn link_xcode_library(name: &str, source: impl AsRef<std::path::Path>) {
|
||||
use std::{path::PathBuf, process::Command};
|
||||
|
||||
let source = source.as_ref();
|
||||
let configuration = if std::env::var("DEBUG")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"Debug"
|
||||
} else {
|
||||
"Release"
|
||||
};
|
||||
|
||||
let (sdk, arch) = match std::env::var("TARGET").unwrap().as_str() {
|
||||
"aarch64-apple-ios" => ("iphoneos", "arm64"),
|
||||
"aarch64-apple-ios-sim" => ("iphonesimulator", "arm64"),
|
||||
"x86_64-apple-ios" => ("iphonesimulator", "x86_64"),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let out_dir = std::env::var_os("OUT_DIR").map(PathBuf::from).unwrap();
|
||||
let derived_data_path = out_dir.join(format!("derivedData-{name}"));
|
||||
|
||||
let status = Command::new("xcodebuild")
|
||||
.arg("build")
|
||||
.arg("-scheme")
|
||||
.arg(name)
|
||||
.arg("-configuration")
|
||||
.arg(configuration)
|
||||
.arg("-sdk")
|
||||
.arg(sdk)
|
||||
.arg("-arch")
|
||||
.arg(arch)
|
||||
.arg("-derivedDataPath")
|
||||
.arg(&derived_data_path)
|
||||
.arg("BUILD_LIBRARY_FOR_DISTRIBUTION=YES")
|
||||
.arg("OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface")
|
||||
.current_dir(source)
|
||||
.env_clear()
|
||||
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
assert!(status.success());
|
||||
|
||||
let lib_out_dir = derived_data_path
|
||||
.join("Build")
|
||||
.join("Products")
|
||||
.join(format!("{configuration}-{sdk}"));
|
||||
|
||||
println!(
|
||||
"cargo::rustc-link-search=framework={}",
|
||||
lib_out_dir.display()
|
||||
);
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
println!("cargo:rustc-link-search=native={}", lib_out_dir.display());
|
||||
println!("cargo:rustc-link-lib=static={name}");
|
||||
}
|
||||
|
||||
/// Updates the Android manifest by inserting XML content into a specified parent tag.
|
||||
///
|
||||
/// The content is wrapped in auto-generated comments and will replace any existing
|
||||
/// content with the same block identifier.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `block_identifier` - A unique identifier for the block (used in comments)
|
||||
/// * `parent` - The parent XML tag name (e.g., "activity", "application")
|
||||
/// * `insert` - The XML content to insert
|
||||
pub fn update_android_manifest(
|
||||
block_identifier: &str,
|
||||
parent: &str,
|
||||
insert: String,
|
||||
) -> anyhow::Result<()> {
|
||||
use std::{
|
||||
env::var_os,
|
||||
fs::{read_to_string, write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
|
||||
let manifest_path = project_path.join("app/src/main/AndroidManifest.xml");
|
||||
if !manifest_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let manifest = read_to_string(&manifest_path)?;
|
||||
let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert);
|
||||
if rewritten != manifest {
|
||||
write(&manifest_path, rewritten)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn xml_block_comment(id: &str) -> String {
|
||||
format!("<!-- {id}. AUTO-GENERATED. DO NOT REMOVE. -->")
|
||||
}
|
||||
|
||||
fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String {
|
||||
let block_comment = xml_block_comment(block_identifier);
|
||||
|
||||
let mut rewritten = Vec::new();
|
||||
let mut found_block = false;
|
||||
let parent_closing_tag = format!("</{parent_tag}>");
|
||||
for line in xml.split('\n') {
|
||||
if line.contains(&block_comment) {
|
||||
found_block = !found_block;
|
||||
continue;
|
||||
}
|
||||
|
||||
// found previous block which should be removed
|
||||
if found_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) = line.find(&parent_closing_tag) {
|
||||
let indentation = " ".repeat(index + 4);
|
||||
rewritten.push(format!("{indentation}{block_comment}"));
|
||||
for l in contents.split('\n') {
|
||||
rewritten.push(format!("{indentation}{l}"));
|
||||
}
|
||||
rewritten.push(format!("{indentation}{block_comment}"));
|
||||
}
|
||||
|
||||
rewritten.push(line.to_string());
|
||||
}
|
||||
|
||||
rewritten.join("\n")
|
||||
}
|
||||
4684
apps/tauri/src-tauri/vendor/tauri-utils/src/config.rs
vendored
Normal file
4684
apps/tauri/src-tauri/vendor/tauri-utils/src/config.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
398
apps/tauri/src-tauri/vendor/tauri-utils/src/config/parse.rs
vendored
Normal file
398
apps/tauri/src-tauri/vendor/tauri-utils/src/config/parse.rs
vendored
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::platform::Target;
|
||||
use json_patch::merge;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// All extensions that are possibly supported, but perhaps not enabled.
|
||||
pub const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
|
||||
|
||||
/// All configuration formats that are possibly supported, but perhaps not enabled.
|
||||
pub const SUPPORTED_FORMATS: &[ConfigFormat] =
|
||||
&[ConfigFormat::Json, ConfigFormat::Json5, ConfigFormat::Toml];
|
||||
|
||||
/// All configuration formats that are currently enabled.
|
||||
pub const ENABLED_FORMATS: &[ConfigFormat] = &[
|
||||
ConfigFormat::Json,
|
||||
#[cfg(feature = "config-json5")]
|
||||
ConfigFormat::Json5,
|
||||
#[cfg(feature = "config-toml")]
|
||||
ConfigFormat::Toml,
|
||||
];
|
||||
|
||||
/// The available configuration formats.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ConfigFormat {
|
||||
/// The default JSON (tauri.conf.json) format.
|
||||
Json,
|
||||
/// The JSON5 (tauri.conf.json5) format.
|
||||
Json5,
|
||||
/// The TOML (Tauri.toml file) format.
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl ConfigFormat {
|
||||
/// Maps the config format to its file name.
|
||||
pub fn into_file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => "tauri.conf.json",
|
||||
Self::Json5 => "tauri.conf.json5",
|
||||
Self::Toml => "Tauri.toml",
|
||||
}
|
||||
}
|
||||
|
||||
fn into_platform_file_name(self, target: Target) -> &'static str {
|
||||
match self {
|
||||
Self::Json => match target {
|
||||
Target::MacOS => "tauri.macos.conf.json",
|
||||
Target::Windows => "tauri.windows.conf.json",
|
||||
Target::Linux => "tauri.linux.conf.json",
|
||||
Target::Android => "tauri.android.conf.json",
|
||||
Target::Ios => "tauri.ios.conf.json",
|
||||
},
|
||||
Self::Json5 => match target {
|
||||
Target::MacOS => "tauri.macos.conf.json5",
|
||||
Target::Windows => "tauri.windows.conf.json5",
|
||||
Target::Linux => "tauri.linux.conf.json5",
|
||||
Target::Android => "tauri.android.conf.json5",
|
||||
Target::Ios => "tauri.ios.conf.json5",
|
||||
},
|
||||
Self::Toml => match target {
|
||||
Target::MacOS => "Tauri.macos.toml",
|
||||
Target::Windows => "Tauri.windows.toml",
|
||||
Target::Linux => "Tauri.linux.toml",
|
||||
Target::Android => "Tauri.android.toml",
|
||||
Target::Ios => "Tauri.ios.toml",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all the errors that can happen while reading the config.
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConfigError {
|
||||
/// Failed to parse from JSON.
|
||||
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
|
||||
FormatJson {
|
||||
/// The path that failed to parse into JSON.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`serde_json::Error`].
|
||||
error: serde_json::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from JSON5.
|
||||
#[cfg(feature = "config-json5")]
|
||||
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
|
||||
FormatJson5 {
|
||||
/// The path that failed to parse into JSON5.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`json5::Error`].
|
||||
error: ::json5::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from TOML.
|
||||
#[cfg(feature = "config-toml")]
|
||||
#[error("unable to parse toml Tauri config file at {path} because {error}")]
|
||||
FormatToml {
|
||||
/// The path that failed to parse into TOML.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`toml::Error`].
|
||||
error: Box<::toml::de::Error>,
|
||||
},
|
||||
|
||||
/// Unknown config file name encountered.
|
||||
#[error("unsupported format encountered {0}")]
|
||||
UnsupportedFormat(String),
|
||||
|
||||
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
|
||||
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
|
||||
DisabledFormat {
|
||||
/// The extension encountered.
|
||||
extension: String,
|
||||
|
||||
/// The cargo feature to enable it.
|
||||
feature: String,
|
||||
},
|
||||
|
||||
/// A generic IO error with context of what caused it.
|
||||
#[error("unable to read Tauri config file at {path} because {error}")]
|
||||
Io {
|
||||
/// The path the IO error occurred on.
|
||||
path: PathBuf,
|
||||
|
||||
/// The [`std::io::Error`].
|
||||
error: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// Determines if the given folder has a configuration file.
|
||||
pub fn folder_has_configuration_file(target: Target, folder: &Path) -> bool {
|
||||
folder.join(ConfigFormat::Json.into_file_name()).exists()
|
||||
|| folder.join(ConfigFormat::Json5.into_file_name()).exists()
|
||||
|| folder.join(ConfigFormat::Toml.into_file_name()).exists()
|
||||
// platform file names
|
||||
|| folder.join(ConfigFormat::Json.into_platform_file_name(target)).exists()
|
||||
|| folder.join(ConfigFormat::Json5.into_platform_file_name(target)).exists()
|
||||
|| folder.join(ConfigFormat::Toml.into_platform_file_name(target)).exists()
|
||||
}
|
||||
|
||||
/// Determines if the given file path represents a Tauri configuration file.
|
||||
pub fn is_configuration_file(target: Target, path: &Path) -> bool {
|
||||
path
|
||||
.file_name()
|
||||
.map(|file_name| {
|
||||
file_name == OsStr::new(ConfigFormat::Json.into_file_name())
|
||||
|| file_name == OsStr::new(ConfigFormat::Json5.into_file_name())
|
||||
|| file_name == OsStr::new(ConfigFormat::Toml.into_file_name())
|
||||
// platform file names
|
||||
|| file_name == OsStr::new(ConfigFormat::Json.into_platform_file_name(target))
|
||||
|| file_name == OsStr::new(ConfigFormat::Json5.into_platform_file_name(target))
|
||||
|| file_name == OsStr::new(ConfigFormat::Toml.into_platform_file_name(target))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Reads the configuration from the given root directory.
|
||||
///
|
||||
/// It first looks for a `tauri.conf.json[5]` or `Tauri.toml` file on the given directory. The file must exist.
|
||||
/// Then it looks for a platform-specific configuration file:
|
||||
/// - `tauri.macos.conf.json[5]` or `Tauri.macos.toml` on macOS
|
||||
/// - `tauri.linux.conf.json[5]` or `Tauri.linux.toml` on Linux
|
||||
/// - `tauri.windows.conf.json[5]` or `Tauri.windows.toml` on Windows
|
||||
/// - `tauri.android.conf.json[5]` or `Tauri.android.toml` on Android
|
||||
/// - `tauri.ios.conf.json[5]` or `Tauri.ios.toml` on iOS
|
||||
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
|
||||
///
|
||||
/// Returns the raw configuration and used config paths.
|
||||
///
|
||||
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
|
||||
pub fn read_from(target: Target, root_dir: &Path) -> Result<(Value, Vec<PathBuf>), ConfigError> {
|
||||
let (mut config, config_file_path) = parse_value(target, root_dir.join("tauri.conf.json"))?;
|
||||
let mut config_paths = vec![config_file_path];
|
||||
if let Some((platform_config, path)) = read_platform(target, root_dir)? {
|
||||
config_paths.push(path);
|
||||
merge(&mut config, &platform_config);
|
||||
}
|
||||
Ok((config, config_paths))
|
||||
}
|
||||
|
||||
/// Reads the platform-specific configuration file from the given root directory if it exists.
|
||||
///
|
||||
/// Check [`read_from`] for more information.
|
||||
pub fn read_platform(
|
||||
target: Target,
|
||||
root_dir: &Path,
|
||||
) -> Result<Option<(Value, PathBuf)>, ConfigError> {
|
||||
let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name(target));
|
||||
if does_supported_file_name_exist(target, &platform_config_path) {
|
||||
let (platform_config, path): (Value, PathBuf) = parse_value(target, platform_config_path)?;
|
||||
Ok(Some((platform_config, path)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a supported config file exists at path.
|
||||
///
|
||||
/// The passed path is expected to be the path to the "default" configuration format, in this case
|
||||
/// JSON with `.json`.
|
||||
pub fn does_supported_file_name_exist(target: Target, path: impl Into<PathBuf>) -> bool {
|
||||
let path = path.into();
|
||||
let source_file_name = path.file_name().unwrap();
|
||||
let lookup_platform_config = ENABLED_FORMATS
|
||||
.iter()
|
||||
.any(|format| source_file_name == format.into_platform_file_name(target));
|
||||
ENABLED_FORMATS.iter().any(|format| {
|
||||
path
|
||||
.with_file_name(if lookup_platform_config {
|
||||
format.into_platform_file_name(target)
|
||||
} else {
|
||||
format.into_file_name()
|
||||
})
|
||||
.exists()
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the config from path, including alternative formats.
|
||||
///
|
||||
/// Hierarchy:
|
||||
/// 1. Check if `tauri.conf.json` exists
|
||||
/// a. Parse it with `serde_json`
|
||||
/// b. Parse it with `json5` if `serde_json` fails
|
||||
/// c. Return original `serde_json` error if all above steps failed
|
||||
/// 2. Check if `tauri.conf.json5` exists
|
||||
/// a. Parse it with `json5`
|
||||
/// b. Return error if all above steps failed
|
||||
/// 3. Check if `Tauri.json` exists
|
||||
/// a. Parse it with `toml`
|
||||
/// b. Return error if all above steps failed
|
||||
/// 4. Return error if all above steps failed
|
||||
pub fn parse(target: Target, path: impl Into<PathBuf>) -> Result<(Config, PathBuf), ConfigError> {
|
||||
do_parse(target, path.into())
|
||||
}
|
||||
|
||||
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
|
||||
pub fn parse_value(
|
||||
target: Target,
|
||||
path: impl Into<PathBuf>,
|
||||
) -> Result<(Value, PathBuf), ConfigError> {
|
||||
do_parse(target, path.into())
|
||||
}
|
||||
|
||||
fn do_parse<D: DeserializeOwned>(
|
||||
target: Target,
|
||||
path: PathBuf,
|
||||
) -> Result<(D, PathBuf), ConfigError> {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
let lookup_platform_config = ENABLED_FORMATS
|
||||
.iter()
|
||||
.any(|format| file_name == format.into_platform_file_name(target));
|
||||
|
||||
let json5 = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Json5.into_platform_file_name(target)
|
||||
} else {
|
||||
ConfigFormat::Json5.into_file_name()
|
||||
});
|
||||
let toml = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Toml.into_platform_file_name(target)
|
||||
} else {
|
||||
ConfigFormat::Toml.into_file_name()
|
||||
});
|
||||
|
||||
let path_ext = path
|
||||
.extension()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
|
||||
if path.exists() {
|
||||
let raw = read_to_string(&path)?;
|
||||
|
||||
// to allow us to easily use the compile-time #[cfg], we always bind
|
||||
#[allow(clippy::let_and_return)]
|
||||
let json = do_parse_json(&raw, &path);
|
||||
|
||||
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
|
||||
// if the json5 is not valid the serde_json error for regular json will be returned.
|
||||
// this could be a bit confusing, so we may want to encourage users using json5 to use the
|
||||
// .json5 extension instead of .json
|
||||
#[cfg(feature = "config-json5")]
|
||||
let json = {
|
||||
match do_parse_json5(&raw, &path) {
|
||||
json5 @ Ok(_) => json5,
|
||||
|
||||
// assume any errors from json5 in a .json file is because it's not json5
|
||||
Err(_) => json,
|
||||
}
|
||||
};
|
||||
|
||||
json.map(|j| (j, path))
|
||||
} else if json5.exists() {
|
||||
#[cfg(feature = "config-json5")]
|
||||
{
|
||||
let raw = read_to_string(&json5)?;
|
||||
do_parse_json5(&raw, &json5).map(|config| (config, json5))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-json5"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".json5".into(),
|
||||
feature: "config-json5".into(),
|
||||
})
|
||||
} else if toml.exists() {
|
||||
#[cfg(feature = "config-toml")]
|
||||
{
|
||||
let raw = read_to_string(&toml)?;
|
||||
do_parse_toml(&raw, &toml).map(|config| (config, toml))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-toml"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".toml".into(),
|
||||
feature: "config-toml".into(),
|
||||
})
|
||||
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
|
||||
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
|
||||
} else {
|
||||
Err(ConfigError::Io {
|
||||
path,
|
||||
error: std::io::ErrorKind::NotFound.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON into a [`Config`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`.
|
||||
pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
|
||||
do_parse_json(raw, path)
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON into a JSON [`Value`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`.
|
||||
pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
|
||||
do_parse_json(raw, path)
|
||||
}
|
||||
|
||||
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON5 into a [`Config`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`. This function requires
|
||||
/// the `config-json5` feature to be enabled.
|
||||
#[cfg(feature = "config-json5")]
|
||||
pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
|
||||
do_parse_json5(raw, path)
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON5 into a JSON [`Value`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`. This function requires
|
||||
/// the `config-json5` feature to be enabled.
|
||||
#[cfg(feature = "config-json5")]
|
||||
pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
|
||||
do_parse_json5(raw, path)
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-json5")]
|
||||
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-toml")]
|
||||
fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
|
||||
path: path.into(),
|
||||
error: Box::new(error),
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
|
||||
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
|
||||
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
3088
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/mod.rs
vendored
Normal file
3088
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/mod.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
255
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/parse.rs
vendored
Normal file
255
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/parse.rs
vendored
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(clippy::result_large_err)]
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// All extensions that are possibly supported, but perhaps not enabled.
|
||||
const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
|
||||
|
||||
/// All configuration formats that are currently enabled.
|
||||
const ENABLED_FORMATS: &[ConfigFormat] = &[
|
||||
ConfigFormat::Json,
|
||||
#[cfg(feature = "config-json5")]
|
||||
ConfigFormat::Json5,
|
||||
#[cfg(feature = "config-toml")]
|
||||
ConfigFormat::Toml,
|
||||
];
|
||||
|
||||
/// The available configuration formats.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum ConfigFormat {
|
||||
/// The default JSON (tauri.conf.json) format.
|
||||
Json,
|
||||
/// The JSON5 (tauri.conf.json5) format.
|
||||
Json5,
|
||||
/// The TOML (Tauri.toml file) format.
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl ConfigFormat {
|
||||
/// Maps the config format to its file name.
|
||||
fn into_file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => "tauri.conf.json",
|
||||
Self::Json5 => "tauri.conf.json5",
|
||||
Self::Toml => "Tauri.toml",
|
||||
}
|
||||
}
|
||||
|
||||
fn into_platform_file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"tauri.macos.conf.json"
|
||||
} else if cfg!(windows) {
|
||||
"tauri.windows.conf.json"
|
||||
} else {
|
||||
"tauri.linux.conf.json"
|
||||
}
|
||||
}
|
||||
Self::Json5 => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"tauri.macos.conf.json5"
|
||||
} else if cfg!(windows) {
|
||||
"tauri.windows.conf.json5"
|
||||
} else {
|
||||
"tauri.linux.conf.json5"
|
||||
}
|
||||
}
|
||||
Self::Toml => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Tauri.macos.toml"
|
||||
} else if cfg!(windows) {
|
||||
"Tauri.windows.toml"
|
||||
} else {
|
||||
"Tauri.linux.toml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all the errors that can happen while reading the config.
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConfigError {
|
||||
/// Failed to parse from JSON.
|
||||
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
|
||||
FormatJson {
|
||||
/// The path that failed to parse into JSON.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`serde_json::Error`].
|
||||
error: serde_json::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from JSON5.
|
||||
#[cfg(feature = "config-json5")]
|
||||
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
|
||||
FormatJson5 {
|
||||
/// The path that failed to parse into JSON5.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`json5::Error`].
|
||||
error: ::json5::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from TOML.
|
||||
#[cfg(feature = "config-toml")]
|
||||
#[error("unable to parse toml Tauri config file at {path} because {error}")]
|
||||
FormatToml {
|
||||
/// The path that failed to parse into TOML.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`toml::Error`].
|
||||
error: ::toml::de::Error,
|
||||
},
|
||||
|
||||
/// Unknown config file name encountered.
|
||||
#[error("unsupported format encountered {0}")]
|
||||
UnsupportedFormat(String),
|
||||
|
||||
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
|
||||
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
|
||||
DisabledFormat {
|
||||
/// The extension encountered.
|
||||
extension: String,
|
||||
|
||||
/// The cargo feature to enable it.
|
||||
feature: String,
|
||||
},
|
||||
|
||||
/// A generic IO error with context of what caused it.
|
||||
#[error("unable to read Tauri config file at {path} because {error}")]
|
||||
Io {
|
||||
/// The path the IO error occurred on.
|
||||
path: PathBuf,
|
||||
|
||||
/// The [`std::io::Error`].
|
||||
error: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
|
||||
pub fn parse_value(path: impl Into<PathBuf>) -> Result<(Value, PathBuf), ConfigError> {
|
||||
do_parse(path.into())
|
||||
}
|
||||
|
||||
fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<(D, PathBuf), ConfigError> {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
let lookup_platform_config = ENABLED_FORMATS
|
||||
.iter()
|
||||
.any(|format| file_name == format.into_platform_file_name());
|
||||
|
||||
let json5 = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Json5.into_platform_file_name()
|
||||
} else {
|
||||
ConfigFormat::Json5.into_file_name()
|
||||
});
|
||||
let toml = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Toml.into_platform_file_name()
|
||||
} else {
|
||||
ConfigFormat::Toml.into_file_name()
|
||||
});
|
||||
|
||||
let path_ext = path
|
||||
.extension()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
|
||||
if path.exists() {
|
||||
let raw = read_to_string(&path)?;
|
||||
|
||||
// to allow us to easily use the compile-time #[cfg], we always bind
|
||||
#[allow(clippy::let_and_return)]
|
||||
let json = do_parse_json(&raw, &path);
|
||||
|
||||
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
|
||||
// if the json5 is not valid the serde_json error for regular json will be returned.
|
||||
// this could be a bit confusing, so we may want to encourage users using json5 to use the
|
||||
// .json5 extension instead of .json
|
||||
#[cfg(feature = "config-json5")]
|
||||
let json = {
|
||||
match do_parse_json5(&raw, &path) {
|
||||
json5 @ Ok(_) => json5,
|
||||
|
||||
// assume any errors from json5 in a .json file is because it's not json5
|
||||
Err(_) => json,
|
||||
}
|
||||
};
|
||||
|
||||
json.map(|j| (j, path))
|
||||
} else if json5.exists() {
|
||||
#[cfg(feature = "config-json5")]
|
||||
{
|
||||
let raw = read_to_string(&json5)?;
|
||||
do_parse_json5(&raw, &path).map(|config| (config, json5))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-json5"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".json5".into(),
|
||||
feature: "config-json5".into(),
|
||||
})
|
||||
} else if toml.exists() {
|
||||
#[cfg(feature = "config-toml")]
|
||||
{
|
||||
let raw = read_to_string(&toml)?;
|
||||
do_parse_toml(&raw, &path).map(|config| (config, toml))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-toml"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".toml".into(),
|
||||
feature: "config-toml".into(),
|
||||
})
|
||||
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
|
||||
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
|
||||
} else {
|
||||
Err(ConfigError::Io {
|
||||
path,
|
||||
error: std::io::ErrorKind::NotFound.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-json5")]
|
||||
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-toml")]
|
||||
fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
|
||||
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
|
||||
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
442
apps/tauri/src-tauri/vendor/tauri-utils/src/html.rs
vendored
Normal file
442
apps/tauri/src-tauri/vendor/tauri-utils/src/html.rs
vendored
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The module to process HTML in Tauri.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use html5ever::{
|
||||
LocalName,
|
||||
interface::QualName,
|
||||
namespace_url, ns,
|
||||
serialize::{HtmlSerializer, SerializeOpts, Serializer, TraversalScope},
|
||||
tendril::TendrilSink,
|
||||
};
|
||||
pub use kuchiki::NodeRef;
|
||||
use kuchiki::{Attribute, ExpandedName, NodeData};
|
||||
use serde::Serialize;
|
||||
#[cfg(feature = "isolation")]
|
||||
use serialize_to_javascript::DefaultTemplate;
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
use crate::pattern::isolation::IsolationJavascriptCodegen;
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config::{DisabledCspModificationKind, PatternKind},
|
||||
};
|
||||
|
||||
// taken from <https://github.com/kuchiki-rs/kuchiki/blob/57ee6920d835315a498e748ba4b07a851ae5e498/src/serializer.rs#L12>
|
||||
fn serialize_node_ref_internal<S: Serializer>(
|
||||
node: &NodeRef,
|
||||
serializer: &mut S,
|
||||
traversal_scope: TraversalScope,
|
||||
) -> crate::Result<()> {
|
||||
match (traversal_scope, node.data()) {
|
||||
(ref scope, NodeData::Element(element)) => {
|
||||
if *scope == TraversalScope::IncludeNode {
|
||||
let attrs = element.attributes.borrow();
|
||||
|
||||
// Unfortunately we need to allocate something to hold these &'a QualName
|
||||
let attrs = attrs
|
||||
.map
|
||||
.iter()
|
||||
.map(|(name, attr)| {
|
||||
(
|
||||
QualName::new(attr.prefix.clone(), name.ns.clone(), name.local.clone()),
|
||||
&attr.value,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
serializer.start_elem(
|
||||
element.name.clone(),
|
||||
attrs.iter().map(|&(ref name, value)| (name, &**value)),
|
||||
)?
|
||||
}
|
||||
|
||||
let children = match element.template_contents.as_ref() {
|
||||
Some(template_root) => template_root.children(),
|
||||
None => node.children(),
|
||||
};
|
||||
for child in children {
|
||||
serialize_node_ref_internal(&child, serializer, TraversalScope::IncludeNode)?
|
||||
}
|
||||
|
||||
if *scope == TraversalScope::IncludeNode {
|
||||
serializer.end_elem(element.name.clone())?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
(_, &NodeData::DocumentFragment) | (_, &NodeData::Document(_)) => {
|
||||
for child in node.children() {
|
||||
serialize_node_ref_internal(&child, serializer, TraversalScope::IncludeNode)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
(TraversalScope::ChildrenOnly(_), _) => Ok(()),
|
||||
|
||||
(TraversalScope::IncludeNode, NodeData::Doctype(doctype)) => {
|
||||
serializer.write_doctype(&doctype.name).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, NodeData::Text(text)) => {
|
||||
serializer.write_text(&text.borrow()).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, NodeData::Comment(text)) => {
|
||||
serializer.write_comment(&text.borrow()).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, NodeData::ProcessingInstruction(contents)) => {
|
||||
let contents = contents.borrow();
|
||||
serializer
|
||||
.write_processing_instruction(&contents.0, &contents.1)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes the node to HTML.
|
||||
pub fn serialize_node(node: &NodeRef) -> Vec<u8> {
|
||||
let mut u8_vec = Vec::new();
|
||||
let mut ser = HtmlSerializer::new(
|
||||
&mut u8_vec,
|
||||
SerializeOpts {
|
||||
traversal_scope: TraversalScope::IncludeNode,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
serialize_node_ref_internal(node, &mut ser, TraversalScope::IncludeNode).unwrap();
|
||||
u8_vec
|
||||
}
|
||||
|
||||
/// Parses the given HTML string.
|
||||
pub fn parse(html: String) -> NodeRef {
|
||||
kuchiki::parse_html().one(html).document_node
|
||||
}
|
||||
|
||||
fn with_head<F: FnOnce(&NodeRef)>(document: &NodeRef, f: F) {
|
||||
if let Ok(ref node) = document.select_first("head") {
|
||||
f(node.as_node())
|
||||
} else {
|
||||
let node = NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("head")),
|
||||
None,
|
||||
);
|
||||
f(&node);
|
||||
document.prepend(node)
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_nonce(document: &NodeRef, selector: &str, token: &str) {
|
||||
if let Ok(elements) = document.select(selector) {
|
||||
for target in elements {
|
||||
let node = target.as_node();
|
||||
let element = node.as_element().unwrap();
|
||||
|
||||
let mut attrs = element.attributes.borrow_mut();
|
||||
// if the node already has the `nonce` attribute, skip it
|
||||
if attrs.get("nonce").is_some() {
|
||||
continue;
|
||||
}
|
||||
attrs.insert("nonce", token.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject nonce tokens to all scripts and styles.
|
||||
pub fn inject_nonce_token(
|
||||
document: &NodeRef,
|
||||
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
|
||||
) {
|
||||
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
|
||||
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
|
||||
}
|
||||
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
|
||||
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
pub fn inject_csp(document: &NodeRef, csp: &str) {
|
||||
with_head(document, |head| {
|
||||
head.append(create_csp_meta_tag(csp));
|
||||
});
|
||||
}
|
||||
|
||||
fn create_csp_meta_tag(csp: &str) -> NodeRef {
|
||||
NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("meta")),
|
||||
vec![
|
||||
(
|
||||
ExpandedName::new(ns!(), LocalName::from("http-equiv")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: "Content-Security-Policy".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
ExpandedName::new(ns!(), LocalName::from("content")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: csp.into(),
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// The shape of the JavaScript Pattern config
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase", tag = "pattern")]
|
||||
pub enum PatternObject {
|
||||
/// Brownfield pattern.
|
||||
Brownfield,
|
||||
/// Isolation pattern. Recommended for security purposes.
|
||||
Isolation {
|
||||
/// Which `IsolationSide` this `PatternObject` is getting injected into
|
||||
side: IsolationSide,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&PatternKind> for PatternObject {
|
||||
fn from(pattern_kind: &PatternKind) -> Self {
|
||||
match pattern_kind {
|
||||
PatternKind::Brownfield => Self::Brownfield,
|
||||
PatternKind::Isolation { .. } => Self::Isolation {
|
||||
side: IsolationSide::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the JavaScript is injected to
|
||||
#[derive(Debug, Serialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum IsolationSide {
|
||||
/// Original frame, the Brownfield application
|
||||
#[default]
|
||||
Original,
|
||||
/// Secure frame, the isolation security application
|
||||
Secure,
|
||||
}
|
||||
|
||||
/// Injects the Isolation JavaScript to a codegen time document.
|
||||
///
|
||||
/// Note: This function is not considered part of the stable API.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub fn inject_codegen_isolation_script(document: &NodeRef) {
|
||||
with_head(document, |head| {
|
||||
let script = NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), "script".into()),
|
||||
vec![(
|
||||
ExpandedName::new(ns!(), LocalName::from("nonce")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: SCRIPT_NONCE_TOKEN.into(),
|
||||
},
|
||||
)],
|
||||
);
|
||||
script.append(NodeRef::new_text(
|
||||
IsolationJavascriptCodegen {}
|
||||
.render_default(&Default::default())
|
||||
.expect("unable to render codegen isolation script template")
|
||||
.into_string(),
|
||||
));
|
||||
|
||||
head.prepend(script);
|
||||
});
|
||||
}
|
||||
|
||||
/// Temporary workaround for Windows not allowing requests
|
||||
///
|
||||
/// Note: this does not prevent path traversal due to the isolation application expectation that it
|
||||
/// is secure.
|
||||
pub fn inline_isolation(document: &NodeRef, dir: &Path) {
|
||||
for script in document
|
||||
.select("script[src]")
|
||||
.expect("unable to parse document for scripts")
|
||||
{
|
||||
let src = {
|
||||
let attributes = script.attributes.borrow();
|
||||
attributes
|
||||
.get(LocalName::from("src"))
|
||||
.expect("script with src attribute has no src value")
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let mut path = PathBuf::from(src);
|
||||
if path.has_root() {
|
||||
path = path
|
||||
.strip_prefix("/")
|
||||
.expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.")
|
||||
.into();
|
||||
}
|
||||
|
||||
let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file");
|
||||
script.as_node().append(NodeRef::new_text(file));
|
||||
|
||||
let mut attributes = script.attributes.borrow_mut();
|
||||
attributes.remove(LocalName::from("src"));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842
|
||||
/// Normalize line endings in script content to match what the browser uses for CSP hashing.
|
||||
///
|
||||
/// According to the HTML spec, browsers normalize:
|
||||
/// - `\r\n` → `\n`
|
||||
/// - `\r` → `\n`
|
||||
pub fn normalize_script_for_csp(input: &[u8]) -> Vec<u8> {
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match input[i] {
|
||||
b'\r' => {
|
||||
if i + 1 < input.len() && input[i + 1] == b'\n' {
|
||||
// CRLF → LF
|
||||
output.push(b'\n');
|
||||
i += 2;
|
||||
} else {
|
||||
// Lone CR → LF
|
||||
output.push(b'\n');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(input[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn csp() {
|
||||
let htmls = vec![
|
||||
"<html><head></head></html>".to_string(),
|
||||
"<html></html>".to_string(),
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let document = parse(html);
|
||||
let csp = "csp-string";
|
||||
inject_csp(&document, csp);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><meta http-equiv="Content-Security-Policy" content="{csp}"></head><body></body></html>"#,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_script_for_csp_test() {
|
||||
let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n";
|
||||
let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n";
|
||||
|
||||
assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_and_serialize_roundtrips() {
|
||||
let htmls = [
|
||||
"<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
|
||||
"<!DOCTYPE html><html><head></head><body></body></html>",
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let parsed = parse(html.to_string());
|
||||
let serialized = serialize_node(&parsed);
|
||||
let result = String::from_utf8(serialized).unwrap();
|
||||
|
||||
assert_eq!(result, html);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_scripts() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><script src="http://example.com/script.js" nonce="{SCRIPT_NONCE_TOKEN}"></script></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_styles() {
|
||||
let html = r#"<html><head><style>body { color: red; }</style></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><style nonce="{STYLE_NONCE_TOKEN}">body {{ color: red; }}</style></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_skips_existing() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js" nonce="existing"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(String::from_utf8(serialize_node(&document)).unwrap(), html);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_respects_disabled_modification() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_isolation_replaces_src_with_content() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap();
|
||||
file.write_all(b"console.log('test');").unwrap();
|
||||
let file_name = file.path().file_name().unwrap().to_str().unwrap();
|
||||
|
||||
let html =
|
||||
format!(r#"<html><head><script src="/{file_name}"></script></head><body></body></html>"#);
|
||||
let document = parse(html);
|
||||
inline_isolation(&document, temp_dir.path());
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
r#"<html><head><script>console.log('test');</script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
335
apps/tauri/src-tauri/vendor/tauri-utils/src/html2.rs
vendored
Normal file
335
apps/tauri/src-tauri/vendor/tauri-utils/src/html2.rs
vendored
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The module to process HTML in Tauri.
|
||||
//!
|
||||
//! # Stability
|
||||
//!
|
||||
//! This is utility used in Tauri internally and not considered part of the stable API.
|
||||
//! If you use it, note that it may include breaking changes in the future.
|
||||
|
||||
use dom_query::NodeRef;
|
||||
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config::DisabledCspModificationKind,
|
||||
};
|
||||
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency might receive updates in minor releases.
|
||||
pub use dom_query::Document;
|
||||
|
||||
/// Serializes the document to HTML.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn serialize_doc(document: &Document) -> Vec<u8> {
|
||||
document.html().as_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Parses the given HTML string.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn parse_doc(html: String) -> Document {
|
||||
Document::from(html)
|
||||
}
|
||||
|
||||
fn ensure_head(document: &Document) -> NodeRef<'_> {
|
||||
document.head().unwrap_or_else(|| {
|
||||
let html = document.html_root();
|
||||
let head = document.tree.new_element("head");
|
||||
html.prepend_child(&head);
|
||||
head
|
||||
})
|
||||
}
|
||||
|
||||
fn inject_nonce(document: &Document, selector: &str, token: &str) {
|
||||
let elements = document.select(selector);
|
||||
for elem in elements.nodes() {
|
||||
// if the node already has the `nonce` attribute, skip it
|
||||
if elem.attr("nonce").is_some() {
|
||||
continue;
|
||||
}
|
||||
elem.set_attr("nonce", token);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject nonce tokens to all scripts and styles.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn inject_nonce_token(
|
||||
document: &Document,
|
||||
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
|
||||
) {
|
||||
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
|
||||
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
|
||||
}
|
||||
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
|
||||
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn inject_csp(document: &Document, csp: &str) {
|
||||
let head = ensure_head(document);
|
||||
let meta_tag = document.tree.new_element("meta");
|
||||
meta_tag.set_attr("http-equiv", "Content-Security-Policy");
|
||||
meta_tag.set_attr("content", csp);
|
||||
head.append_child(&meta_tag);
|
||||
}
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn append_script_to_head(document: &Document, script: &str) {
|
||||
let head = ensure_head(document);
|
||||
let script_tag = document.tree.new_element("script");
|
||||
script_tag.set_text(script);
|
||||
head.prepend_child(&script_tag);
|
||||
}
|
||||
|
||||
/// Injects the Isolation JavaScript to a codegen time document.
|
||||
///
|
||||
/// Note: This function is not considered part of the stable API.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub fn inject_codegen_isolation_script(document: &Document) {
|
||||
use crate::pattern::isolation::IsolationJavascriptCodegen;
|
||||
use serialize_to_javascript::DefaultTemplate;
|
||||
|
||||
let head = ensure_head(document);
|
||||
|
||||
let script_content = IsolationJavascriptCodegen {}
|
||||
.render_default(&Default::default())
|
||||
.expect("unable to render codegen isolation script template")
|
||||
.into_string();
|
||||
|
||||
let script_tag = document.tree.new_element("script");
|
||||
script_tag.set_attr("nonce", SCRIPT_NONCE_TOKEN);
|
||||
script_tag.set_text(script_content);
|
||||
|
||||
head.prepend_child(&script_tag);
|
||||
}
|
||||
|
||||
/// Temporary workaround for Windows not allowing requests
|
||||
///
|
||||
/// Note: this does not prevent path traversal due to the isolation application expectation that it
|
||||
/// is secure.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub fn inline_isolation(document: &Document, dir: &std::path::Path) {
|
||||
let scripts = document.select("script[src]");
|
||||
|
||||
for script in scripts.nodes() {
|
||||
let src = match script.attr("src") {
|
||||
Some(s) => s.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut path = std::path::PathBuf::from(src);
|
||||
if path.has_root() {
|
||||
path = path
|
||||
.strip_prefix("/")
|
||||
.expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.")
|
||||
.into();
|
||||
}
|
||||
|
||||
let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file");
|
||||
|
||||
script.set_text(file);
|
||||
script.remove_attr("src");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842
|
||||
/// Normalize line endings in script content to match what the browser uses for CSP hashing.
|
||||
///
|
||||
/// According to the HTML spec, browsers normalize:
|
||||
/// - `\r\n` → `\n`
|
||||
/// - `\r` → `\n`
|
||||
pub fn normalize_script_for_csp(input: &[u8]) -> Vec<u8> {
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match input[i] {
|
||||
b'\r' => {
|
||||
if i + 1 < input.len() && input[i + 1] == b'\n' {
|
||||
// CRLF → LF
|
||||
output.push(b'\n');
|
||||
i += 2;
|
||||
} else {
|
||||
// Lone CR → LF
|
||||
output.push(b'\n');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(input[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn csp() {
|
||||
let htmls = vec![
|
||||
"<html><head></head></html>".to_string(),
|
||||
"<html></html>".to_string(),
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let document = parse_doc(html);
|
||||
let csp = "csp-string";
|
||||
inject_csp(&document, csp);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><meta http-equiv="Content-Security-Policy" content="{csp}"></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_script_for_csp_test() {
|
||||
let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n";
|
||||
let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n";
|
||||
|
||||
assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_and_serialize_roundtrips() {
|
||||
let htmls = [
|
||||
"<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
|
||||
"<!DOCTYPE html><html><head></head><body></body></html>",
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let parsed = parse_doc(html.to_string());
|
||||
let serialized = serialize_doc(&parsed);
|
||||
let result = String::from_utf8(serialized).unwrap();
|
||||
|
||||
assert_eq!(result, html);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_scripts() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><script src="http://example.com/script.js" nonce="{SCRIPT_NONCE_TOKEN}"></script></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_styles() {
|
||||
let html = r#"<html><head><style>body { color: red; }</style></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><style nonce="{STYLE_NONCE_TOKEN}">body {{ color: red; }}</style></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_script_to_head_test() {
|
||||
let html = r#"<html><head></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
append_script_to_head(&document, r#"console.log('Test')"#);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(r#"<html><head><script>console.log('Test')</script></head><body></body></html>"#)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_skips_existing() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js" nonce="existing"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(String::from_utf8(serialize_doc(&document)).unwrap(), html);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_respects_disabled_modification() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "isolation")]
|
||||
fn inline_isolation_replaces_src_with_content() {
|
||||
use std::io::Write;
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap();
|
||||
file.write_all(b"console.log('test');").unwrap();
|
||||
let file_name = file.path().file_name().unwrap().to_str().unwrap();
|
||||
|
||||
let html =
|
||||
format!(r#"<html><head><script src="/{file_name}"></script></head><body></body></html>"#);
|
||||
let document = parse_doc(html);
|
||||
inline_isolation(&document, temp_dir.path());
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
r#"<html><head><script>console.log('test');</script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
47
apps/tauri/src-tauri/vendor/tauri-utils/src/io.rs
vendored
Normal file
47
apps/tauri/src-tauri/vendor/tauri-utils/src/io.rs
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! IO helpers.
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
/// Read all bytes until a newline (the `0xA` byte) or a carriage return (`\r`) is reached, and append them to the provided buffer.
|
||||
///
|
||||
/// Adapted from <https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line>.
|
||||
pub fn read_line<R: BufRead + ?Sized>(r: &mut R, buf: &mut Vec<u8>) -> std::io::Result<usize> {
|
||||
let mut read = 0;
|
||||
loop {
|
||||
let (done, used) = {
|
||||
let available = match r.fill_buf() {
|
||||
Ok(n) => n,
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match memchr::memchr(b'\n', available) {
|
||||
Some(i) => {
|
||||
let end = i + 1;
|
||||
buf.extend_from_slice(&available[..end]);
|
||||
(true, end)
|
||||
}
|
||||
None => match memchr::memchr(b'\r', available) {
|
||||
Some(i) => {
|
||||
let end = i + 1;
|
||||
buf.extend_from_slice(&available[..end]);
|
||||
(true, end)
|
||||
}
|
||||
None => {
|
||||
buf.extend_from_slice(available);
|
||||
(false, available.len())
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
r.consume(used);
|
||||
read += used;
|
||||
if done || used == 0 {
|
||||
return Ok(read);
|
||||
}
|
||||
}
|
||||
}
|
||||
404
apps/tauri/src-tauri/vendor/tauri-utils/src/lib.rs
vendored
Normal file
404
apps/tauri/src-tauri/vendor/tauri-utils/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! This crate contains common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets.
|
||||
|
||||
#![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"
|
||||
)]
|
||||
#![warn(missing_docs, rust_2018_idioms)]
|
||||
#![allow(clippy::deprecated_semver)]
|
||||
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub mod acl;
|
||||
pub mod assets;
|
||||
pub mod config;
|
||||
pub mod config_v1;
|
||||
#[cfg(feature = "html-manipulation")]
|
||||
pub mod html;
|
||||
#[cfg(feature = "html-manipulation-2")]
|
||||
pub mod html2;
|
||||
pub mod io;
|
||||
pub mod mime_type;
|
||||
pub mod platform;
|
||||
pub mod plugin;
|
||||
/// Prepare application resources and sidecars.
|
||||
#[cfg(feature = "resources")]
|
||||
pub mod resources;
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub mod tokens;
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub mod build;
|
||||
|
||||
/// Application pattern.
|
||||
pub mod pattern;
|
||||
|
||||
/// `tauri::App` package information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PackageInfo {
|
||||
/// App name
|
||||
pub name: String,
|
||||
/// App version
|
||||
pub version: Version,
|
||||
/// The crate authors.
|
||||
pub authors: &'static str,
|
||||
/// The crate description.
|
||||
pub description: &'static str,
|
||||
/// The crate name.
|
||||
pub crate_name: &'static str,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
mod window_effects {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Platform-specific window effects
|
||||
pub enum WindowEffect {
|
||||
/// A default material appropriate for the view's effectiveAppearance. **macOS 10.14-**
|
||||
#[deprecated(
|
||||
since = "macOS 10.14",
|
||||
note = "You should instead choose an appropriate semantic material."
|
||||
)]
|
||||
AppearanceBased,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
Light,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
Dark,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
MediumLight,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
UltraDark,
|
||||
/// **macOS 10.10+**
|
||||
Titlebar,
|
||||
/// **macOS 10.10+**
|
||||
Selection,
|
||||
/// **macOS 10.11+**
|
||||
Menu,
|
||||
/// **macOS 10.11+**
|
||||
Popover,
|
||||
/// **macOS 10.11+**
|
||||
Sidebar,
|
||||
/// **macOS 10.14+**
|
||||
HeaderView,
|
||||
/// **macOS 10.14+**
|
||||
Sheet,
|
||||
/// **macOS 10.14+**
|
||||
WindowBackground,
|
||||
/// **macOS 10.14+**
|
||||
HudWindow,
|
||||
/// **macOS 10.14+**
|
||||
FullScreenUI,
|
||||
/// **macOS 10.14+**
|
||||
Tooltip,
|
||||
/// **macOS 10.14+**
|
||||
ContentBackground,
|
||||
/// **macOS 10.14+**
|
||||
UnderWindowBackground,
|
||||
/// **macOS 10.14+**
|
||||
UnderPageBackground,
|
||||
/// Mica effect that matches the system dark preference **Windows 11 Only**
|
||||
Mica,
|
||||
/// Mica effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**
|
||||
MicaDark,
|
||||
/// Mica effect with light mode **Windows 11 Only**
|
||||
MicaLight,
|
||||
/// Tabbed effect that matches the system dark preference **Windows 11 Only**
|
||||
Tabbed,
|
||||
/// Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**
|
||||
TabbedDark,
|
||||
/// Tabbed effect with light mode **Windows 11 Only**
|
||||
TabbedLight,
|
||||
/// **Windows 7/10/11(22H1) Only**
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// This effect has bad performance when resizing/dragging the window on Windows 11 build 22621.
|
||||
Blur,
|
||||
/// **Windows 10/11 Only**
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000.
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
/// Window effect state **macOS only**
|
||||
///
|
||||
/// <https://developer.apple.com/documentation/appkit/nsvisualeffectview/state>
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum WindowEffectState {
|
||||
/// Make window effect state follow the window's active state
|
||||
FollowsWindowActiveState,
|
||||
/// Make window effect state always active
|
||||
Active,
|
||||
/// Make window effect state always inactive
|
||||
Inactive,
|
||||
}
|
||||
}
|
||||
|
||||
pub use window_effects::{WindowEffect, WindowEffectState};
|
||||
|
||||
/// How the window title bar should be displayed on macOS.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Default)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub enum TitleBarStyle {
|
||||
/// A normal title bar.
|
||||
#[default]
|
||||
Visible,
|
||||
/// Makes the title bar transparent, so the window background color is shown instead.
|
||||
///
|
||||
/// Useful if you don't need to have actual HTML under the title bar. This lets you avoid the caveats of using `TitleBarStyle::Overlay`. Will be more useful when Tauri lets you set a custom window background color.
|
||||
Transparent,
|
||||
/// Shows the title bar as a transparent overlay over the window's content.
|
||||
///
|
||||
/// Keep in mind:
|
||||
/// - The height of the title bar is different on different OS versions, which can lead to window the controls and title not being where you don't expect.
|
||||
/// - You need to define a custom drag region to make your window draggable, however due to a limitation you can't drag the window when it's not in focus <https://github.com/tauri-apps/tauri/issues/4316>.
|
||||
/// - The color of the window title depends on the system theme.
|
||||
Overlay,
|
||||
}
|
||||
|
||||
impl Serialize for TitleBarStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TitleBarStyle {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"transparent" => Self::Transparent,
|
||||
"overlay" => Self::Overlay,
|
||||
_ => Self::Visible,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TitleBarStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Visible => "Visible",
|
||||
Self::Transparent => "Transparent",
|
||||
Self::Overlay => "Overlay",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// System theme.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub enum Theme {
|
||||
/// Light theme.
|
||||
Light,
|
||||
/// Dark theme.
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl Serialize for Theme {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Theme {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"dark" => Self::Dark,
|
||||
_ => Self::Light,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Theme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Light => "light",
|
||||
Self::Dark => "dark",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about environment variables.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct Env {
|
||||
/// The APPIMAGE environment variable.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub appimage: Option<std::ffi::OsString>,
|
||||
/// The APPDIR environment variable.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub appdir: Option<std::ffi::OsString>,
|
||||
/// The command line arguments of the current process.
|
||||
pub args_os: Vec<OsString>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Env {
|
||||
fn default() -> Self {
|
||||
let args_os = std::env::args_os().collect();
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let env = Self {
|
||||
#[cfg(target_os = "linux")]
|
||||
appimage: std::env::var_os("APPIMAGE"),
|
||||
#[cfg(target_os = "linux")]
|
||||
appdir: std::env::var_os("APPDIR"),
|
||||
args_os,
|
||||
};
|
||||
if env.appimage.is_some() || env.appdir.is_some() {
|
||||
// validate that we're actually running on an AppImage
|
||||
// an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}`
|
||||
// see <https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501>
|
||||
// note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage.
|
||||
let is_temp = std::env::current_exe()
|
||||
.map(|p| {
|
||||
p.display()
|
||||
.to_string()
|
||||
.starts_with(&format!("{}/.mount_", std::env::temp_dir().display()))
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if !is_temp {
|
||||
log::warn!(
|
||||
"`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue."
|
||||
);
|
||||
}
|
||||
}
|
||||
env
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Self { args_os }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result type of `tauri-utils`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The error type of `tauri-utils`.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Target triple architecture error
|
||||
#[error("Unable to determine target-architecture")]
|
||||
Architecture,
|
||||
/// Target triple OS error
|
||||
#[error("Unable to determine target-os")]
|
||||
Os,
|
||||
/// Target triple environment error
|
||||
#[error("Unable to determine target-environment")]
|
||||
Environment,
|
||||
/// Tried to get resource on an unsupported platform
|
||||
#[error("Unsupported platform for reading resources")]
|
||||
UnsupportedPlatform,
|
||||
/// Get parent process error
|
||||
#[error("Could not get parent process")]
|
||||
ParentProcess,
|
||||
/// Get parent process PID error
|
||||
#[error("Could not get parent PID")]
|
||||
ParentPid,
|
||||
/// Get child process error
|
||||
#[error("Could not get child process")]
|
||||
ChildProcess,
|
||||
/// IO error
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Invalid pattern.
|
||||
#[error("invalid pattern `{0}`. Expected either `brownfield` or `isolation`.")]
|
||||
InvalidPattern(String),
|
||||
/// Invalid glob pattern.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("{0}")]
|
||||
GlobPattern(#[from] glob::PatternError),
|
||||
/// Failed to use glob pattern.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("`{0}`")]
|
||||
Glob(#[from] glob::GlobError),
|
||||
/// Glob pattern did not find any results.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("glob pattern {0} path not found or didn't match any files.")]
|
||||
GlobPathNotFound(String),
|
||||
/// Error walking directory.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("{0}")]
|
||||
WalkdirError(#[from] walkdir::Error),
|
||||
/// Not allowed to walk dir.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error(
|
||||
"could not walk directory `{0}`, try changing `allow_walk` to true on the `ResourcePaths` constructor."
|
||||
)]
|
||||
NotAllowedToWalkDir(std::path::PathBuf),
|
||||
/// Resource path doesn't exist
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("resource path `{0}` doesn't exist")]
|
||||
ResourcePathNotFound(std::path::PathBuf),
|
||||
}
|
||||
|
||||
/// Reconstructs a path from its components using the platform separator then converts it to String and removes UNC prefixes on Windows if it exists.
|
||||
pub fn display_path<P: AsRef<Path>>(p: P) -> String {
|
||||
dunce::simplified(&p.as_ref().components().collect::<PathBuf>())
|
||||
.display()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Write the file only if the content of the existing file (if any) is different.
|
||||
///
|
||||
/// This will always write unless the file exists with identical content.
|
||||
pub fn write_if_changed<P, C>(path: P, content: C) -> std::io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
C: AsRef<[u8]>,
|
||||
{
|
||||
if let Ok(existing) = std::fs::read(&path)
|
||||
&& existing == content.as_ref()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
std::fs::write(path, content)
|
||||
}
|
||||
154
apps/tauri/src-tauri/vendor/tauri-utils/src/mime_type.rs
vendored
Normal file
154
apps/tauri/src-tauri/vendor/tauri-utils/src/mime_type.rs
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Determine a mime type from a URI or file contents.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
const MIMETYPE_PLAIN: &str = "text/plain";
|
||||
|
||||
/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers)
|
||||
#[allow(missing_docs)]
|
||||
pub enum MimeType {
|
||||
Css,
|
||||
Csv,
|
||||
Html,
|
||||
Ico,
|
||||
Js,
|
||||
Json,
|
||||
Jsonld,
|
||||
Mp4,
|
||||
OctetStream,
|
||||
Rtf,
|
||||
Svg,
|
||||
Txt,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MimeType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mime = match self {
|
||||
MimeType::Css => "text/css",
|
||||
MimeType::Csv => "text/csv",
|
||||
MimeType::Html => "text/html",
|
||||
MimeType::Ico => "image/vnd.microsoft.icon",
|
||||
MimeType::Js => "text/javascript",
|
||||
MimeType::Json => "application/json",
|
||||
MimeType::Jsonld => "application/ld+json",
|
||||
MimeType::Mp4 => "video/mp4",
|
||||
MimeType::OctetStream => "application/octet-stream",
|
||||
MimeType::Rtf => "application/rtf",
|
||||
MimeType::Svg => "image/svg+xml",
|
||||
MimeType::Txt => MIMETYPE_PLAIN,
|
||||
};
|
||||
write!(f, "{mime}")
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType.
|
||||
pub fn parse_from_uri(uri: &str) -> MimeType {
|
||||
Self::parse_from_uri_with_fallback(uri, Self::Html)
|
||||
}
|
||||
|
||||
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType with specified fallback for unknown file extensions.
|
||||
pub fn parse_from_uri_with_fallback(uri: &str, fallback: MimeType) -> MimeType {
|
||||
let suffix = uri.split('.').next_back();
|
||||
match suffix {
|
||||
Some("bin") => Self::OctetStream,
|
||||
Some("css" | "less" | "sass" | "styl") => Self::Css,
|
||||
Some("csv") => Self::Csv,
|
||||
Some("html") => Self::Html,
|
||||
Some("ico") => Self::Ico,
|
||||
Some("js") => Self::Js,
|
||||
Some("json") => Self::Json,
|
||||
Some("jsonld") => Self::Jsonld,
|
||||
Some("mjs") => Self::Js,
|
||||
Some("mp4") => Self::Mp4,
|
||||
Some("rtf") => Self::Rtf,
|
||||
Some("svg") => Self::Svg,
|
||||
Some("txt") => Self::Txt,
|
||||
// Assume HTML when a TLD is found for eg. `wry:://tauri.app` | `wry://hello.com`
|
||||
Some(_) => fallback,
|
||||
// using octet stream according to this:
|
||||
// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types>
|
||||
None => Self::OctetStream,
|
||||
}
|
||||
}
|
||||
|
||||
/// infer mimetype from content (or) URI if needed.
|
||||
pub fn parse(content: &[u8], uri: &str) -> String {
|
||||
Self::parse_with_fallback(content, uri, Self::Html)
|
||||
}
|
||||
/// infer mimetype from content (or) URI if needed with specified fallback for unknown file extensions.
|
||||
pub fn parse_with_fallback(content: &[u8], uri: &str, fallback: MimeType) -> String {
|
||||
let mime = if uri.ends_with(".svg") {
|
||||
// when reading svg, we can't use `infer`
|
||||
None
|
||||
} else {
|
||||
infer::get(content).map(|info| info.mime_type())
|
||||
};
|
||||
|
||||
match mime {
|
||||
Some(mime) if mime == MIMETYPE_PLAIN => {
|
||||
Self::parse_from_uri_with_fallback(uri, fallback).to_string()
|
||||
}
|
||||
None => Self::parse_from_uri_with_fallback(uri, fallback).to_string(),
|
||||
Some(mime) => mime.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_parse_mimetype_from_uri() {
|
||||
let css = MimeType::parse_from_uri(
|
||||
"https://unpkg.com/browse/bootstrap@4.1.0/dist/css/bootstrap-grid.css",
|
||||
)
|
||||
.to_string();
|
||||
assert_eq!(css, "text/css".to_string());
|
||||
|
||||
let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string();
|
||||
assert_eq!(csv, "text/csv".to_string());
|
||||
|
||||
let ico: String =
|
||||
MimeType::parse_from_uri("https://icons.duckduckgo.com/ip3/microsoft.com.ico").to_string();
|
||||
assert_eq!(ico, String::from("image/vnd.microsoft.icon"));
|
||||
|
||||
let html: String = MimeType::parse_from_uri("https://tauri.app/index.html").to_string();
|
||||
assert_eq!(html, String::from("text/html"));
|
||||
|
||||
let js: String =
|
||||
MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js")
|
||||
.to_string();
|
||||
assert_eq!(js, "text/javascript".to_string());
|
||||
|
||||
let json: String =
|
||||
MimeType::parse_from_uri("https://unpkg.com/browse/react@17.0.1/build-info.json").to_string();
|
||||
assert_eq!(json, String::from("application/json"));
|
||||
|
||||
let jsonld: String = MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string();
|
||||
assert_eq!(jsonld, String::from("application/ld+json"));
|
||||
|
||||
let mjs: String = MimeType::parse_from_uri("https://example.com/bundled.mjs").to_string();
|
||||
assert_eq!(mjs, String::from("text/javascript"));
|
||||
|
||||
let mp4: String = MimeType::parse_from_uri("https://example.com/video.mp4").to_string();
|
||||
assert_eq!(mp4, String::from("video/mp4"));
|
||||
|
||||
let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string();
|
||||
assert_eq!(rtf, String::from("application/rtf"));
|
||||
|
||||
let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string();
|
||||
assert_eq!(svg, String::from("image/svg+xml"));
|
||||
|
||||
let txt: String = MimeType::parse_from_uri("https://example.com/file.txt").to_string();
|
||||
assert_eq!(txt, String::from("text/plain"));
|
||||
|
||||
let custom_scheme = MimeType::parse_from_uri("wry://tauri.app").to_string();
|
||||
assert_eq!(custom_scheme, String::from("text/html"));
|
||||
}
|
||||
}
|
||||
154
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.js
vendored
Normal file
154
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.js
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* IMPORTANT: See ipc.js for the main frame implementation.
|
||||
* main frame -> isolation frame = isolation payload
|
||||
* isolation frame -> main frame = isolation message
|
||||
*/
|
||||
|
||||
;(async function () {
|
||||
/**
|
||||
* Sends the message to the isolation frame.
|
||||
* @param {any} message
|
||||
*/
|
||||
function sendMessage(message) {
|
||||
window.parent.postMessage(message, '*')
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {string} - The main frame origin.
|
||||
*/
|
||||
const origin = __TEMPLATE_origin__
|
||||
|
||||
/**
|
||||
* @type {Uint8Array} - Injected by Tauri during runtime
|
||||
*/
|
||||
const aesGcmKeyRaw = new Uint8Array(__TEMPLATE_runtime_aes_gcm_key__)
|
||||
|
||||
/**
|
||||
* @type {CryptoKey}
|
||||
*/
|
||||
const aesGcmKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
aesGcmKeyRaw,
|
||||
'AES-GCM',
|
||||
false,
|
||||
['encrypt']
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {object} data
|
||||
* @return {Promise<{nonce: number[], payload: number[]}>}
|
||||
*/
|
||||
async function encrypt(payload) {
|
||||
const algorithm = Object.create(null)
|
||||
algorithm.name = 'AES-GCM'
|
||||
algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
|
||||
|
||||
const { contentType, data } = __RAW_process_ipc_message_fn__(payload)
|
||||
|
||||
const message =
|
||||
typeof data === 'string'
|
||||
? new TextEncoder().encode(data)
|
||||
: ArrayBuffer.isView(data) || data instanceof ArrayBuffer
|
||||
? data
|
||||
: new Uint8Array(data)
|
||||
|
||||
return window.crypto.subtle
|
||||
.encrypt(algorithm, aesGcmKey, message)
|
||||
.then((payload) => {
|
||||
const result = Object.create(null)
|
||||
result.nonce = Array.from(new Uint8Array(algorithm.iv))
|
||||
result.payload = Array.from(new Uint8Array(payload))
|
||||
result.contentType = contentType
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a message event is a valid isolation message.
|
||||
*
|
||||
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation message
|
||||
* @return {boolean} - if the event was a valid isolation message
|
||||
*/
|
||||
function isIsolationMessage(data) {
|
||||
if (typeof data === 'object' && typeof data.payload === 'object') {
|
||||
const keys = data.payload ? Object.keys(data.payload) : []
|
||||
return (
|
||||
keys.length > 0
|
||||
&& keys.every(
|
||||
(key) => key === 'nonce' || key === 'payload' || key === 'contentType'
|
||||
)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a message event is a valid isolation payload.
|
||||
*
|
||||
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation payload
|
||||
* @return boolean
|
||||
*/
|
||||
function isIsolationPayload(data) {
|
||||
return (
|
||||
typeof data === 'object'
|
||||
&& 'callback' in data
|
||||
&& 'error' in data
|
||||
&& !isIsolationMessage(data)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming payload events.
|
||||
* @param {MessageEvent<any>} event
|
||||
*/
|
||||
async function payloadHandler(event) {
|
||||
if (event.origin !== origin || !isIsolationPayload(event.data)) {
|
||||
return
|
||||
}
|
||||
|
||||
let data = event.data
|
||||
|
||||
if (typeof window.__TAURI_ISOLATION_HOOK__ === 'function') {
|
||||
// await even if it's not async so that we can support async ones
|
||||
data = await window.__TAURI_ISOLATION_HOOK__(data)
|
||||
}
|
||||
|
||||
const message = Object.create(null)
|
||||
message.cmd = data.cmd
|
||||
message.callback = data.callback
|
||||
message.error = data.error
|
||||
message.options = data.options
|
||||
message.payload = await encrypt(data.payload)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
window.addEventListener('message', payloadHandler, false)
|
||||
|
||||
/**
|
||||
* @type {number} - How many milliseconds to wait between ready checks
|
||||
*/
|
||||
const readyIntervalMs = 50
|
||||
|
||||
/**
|
||||
* Wait until this Isolation context is ready to receive messages, and let the main frame know.
|
||||
*/
|
||||
function waitUntilReady() {
|
||||
// consider either a function or an explicitly set null value as the ready signal
|
||||
if (
|
||||
typeof window.__TAURI_ISOLATION_HOOK__ === 'function'
|
||||
|| window.__TAURI_ISOLATION_HOOK__ === null
|
||||
) {
|
||||
sendMessage('__TAURI_ISOLATION_READY__')
|
||||
} else {
|
||||
setTimeout(waitUntilReady, readyIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(waitUntilReady, readyIntervalMs)
|
||||
})()
|
||||
|
||||
document.currentScript?.remove()
|
||||
171
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.rs
vendored
Normal file
171
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.rs
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::array::TryFromSliceError;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use aes_gcm::aead::Aead;
|
||||
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||
use getrandom::Error as CsprngError;
|
||||
use serialize_to_javascript::{Template, default_template};
|
||||
|
||||
/// The style for the isolation iframe.
|
||||
pub const IFRAME_STYLE: &str = "#__tauri_isolation__ { display: none !important }";
|
||||
|
||||
/// Errors that can occur during Isolation keys generation.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Something went wrong with the CSPRNG.
|
||||
#[error("CSPRNG error")]
|
||||
Csprng(#[from] CsprngError),
|
||||
|
||||
/// Something went wrong with decrypting an AES-GCM payload
|
||||
#[error("AES-GCM")]
|
||||
Aes,
|
||||
|
||||
/// Nonce was not 96 bits
|
||||
#[error("Nonce: {0}")]
|
||||
NonceSize(#[from] TryFromSliceError),
|
||||
|
||||
/// Payload was not valid utf8
|
||||
#[error("{0}")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
|
||||
/// Invalid json format
|
||||
#[error("{0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// A formatted AES-GCM cipher instance along with the key used to initialize it.
|
||||
#[derive(Clone)]
|
||||
pub struct AesGcmPair {
|
||||
raw: [u8; 32],
|
||||
key: Aes256Gcm,
|
||||
}
|
||||
|
||||
impl Debug for AesGcmPair {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "AesGcmPair(...)")
|
||||
}
|
||||
}
|
||||
|
||||
impl AesGcmPair {
|
||||
fn new() -> Result<Self, Error> {
|
||||
let mut raw = [0u8; 32];
|
||||
getrandom::fill(&mut raw)?;
|
||||
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&raw);
|
||||
Ok(Self {
|
||||
raw,
|
||||
key: Aes256Gcm::new(key),
|
||||
})
|
||||
}
|
||||
|
||||
/// The raw value used to create the AES-GCM key
|
||||
pub fn raw(&self) -> &[u8; 32] {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
/// The formatted AES-GCM key
|
||||
pub fn key(&self) -> &Aes256Gcm {
|
||||
&self.key
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn encrypt(&self, nonce: &[u8; 12], payload: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
self
|
||||
.key
|
||||
.encrypt(nonce.into(), payload)
|
||||
.map_err(|_| self::Error::Aes)
|
||||
}
|
||||
}
|
||||
|
||||
/// All cryptographic keys required for Isolation encryption
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Keys {
|
||||
/// AES-GCM key
|
||||
aes_gcm: AesGcmPair,
|
||||
}
|
||||
|
||||
impl Keys {
|
||||
/// Securely generate required keys for Isolation encryption.
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
AesGcmPair::new().map(|aes_gcm| Self { aes_gcm })
|
||||
}
|
||||
|
||||
/// The AES-GCM data (and raw data).
|
||||
pub fn aes_gcm(&self) -> &AesGcmPair {
|
||||
&self.aes_gcm
|
||||
}
|
||||
|
||||
/// Decrypts a message using the generated keys.
|
||||
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<Vec<u8>, Error> {
|
||||
let RawIsolationPayload { nonce, payload, .. } = raw;
|
||||
let nonce: [u8; 12] = nonce.as_ref().try_into()?;
|
||||
self
|
||||
.aes_gcm
|
||||
.key
|
||||
.decrypt(Nonce::from_slice(&nonce), payload.as_ref())
|
||||
.map_err(|_| self::Error::Aes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw representation of
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawIsolationPayload<'a> {
|
||||
nonce: Cow<'a, [u8]>,
|
||||
payload: Cow<'a, [u8]>,
|
||||
content_type: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> RawIsolationPayload<'a> {
|
||||
/// Content type of this payload.
|
||||
pub fn content_type(&self) -> &Cow<'a, str> {
|
||||
&self.content_type
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Vec<u8>> for RawIsolationPayload<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &'a Vec<u8>) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(value).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Isolation JavaScript template meant to be injected during codegen.
|
||||
///
|
||||
/// Note: This struct is not considered part of the stable API
|
||||
#[derive(Template)]
|
||||
#[default_template("isolation.js")]
|
||||
pub struct IsolationJavascriptCodegen {
|
||||
// this template intentionally does not include the runtime field
|
||||
}
|
||||
|
||||
/// The Isolation JavaScript template meant to be injected during runtime.
|
||||
///
|
||||
/// Note: This struct is not considered part of the stable API
|
||||
#[derive(Template)]
|
||||
#[default_template("isolation.js")]
|
||||
pub struct IsolationJavascriptRuntime<'a> {
|
||||
/// The key used on the Rust backend and the Isolation Javascript
|
||||
pub runtime_aes_gcm_key: &'a [u8; 32],
|
||||
/// The origin the isolation application is expecting messages from.
|
||||
pub origin: String,
|
||||
/// The function that processes the IPC message.
|
||||
#[raw]
|
||||
pub process_ipc_message_fn: &'a str,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn create_keys() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = super::Keys::new()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
7
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/mod.rs
vendored
Normal file
7
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// Handling the Tauri "Isolation" Pattern.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub mod isolation;
|
||||
434
apps/tauri/src-tauri/vendor/tauri-utils/src/platform.rs
vendored
Normal file
434
apps/tauri/src-tauri/vendor/tauri-utils/src/platform.rs
vendored
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Platform helper functions.
|
||||
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Env, PackageInfo, config::BundleType};
|
||||
|
||||
mod starting_binary;
|
||||
|
||||
/// URI prefix of a Tauri asset.
|
||||
///
|
||||
/// This is referenced in the Tauri Android library,
|
||||
/// which resolves these assets to a file descriptor.
|
||||
#[cfg(target_os = "android")]
|
||||
pub const ANDROID_ASSET_PROTOCOL_URI_PREFIX: &str = "asset://localhost/";
|
||||
|
||||
/// Platform target.
|
||||
#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[non_exhaustive]
|
||||
pub enum Target {
|
||||
/// MacOS.
|
||||
#[serde(rename = "macOS")]
|
||||
MacOS,
|
||||
/// Windows.
|
||||
Windows,
|
||||
/// Linux.
|
||||
Linux,
|
||||
/// Android.
|
||||
Android,
|
||||
/// iOS.
|
||||
#[serde(rename = "iOS")]
|
||||
Ios,
|
||||
}
|
||||
|
||||
impl Display for Target {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::MacOS => "macOS",
|
||||
Self::Windows => "windows",
|
||||
Self::Linux => "linux",
|
||||
Self::Android => "android",
|
||||
Self::Ios => "iOS",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// Parses the target from the given target triple.
|
||||
pub fn from_triple(target: &str) -> Self {
|
||||
if target.contains("darwin") {
|
||||
Self::MacOS
|
||||
} else if target.contains("windows") {
|
||||
Self::Windows
|
||||
} else if target.contains("android") {
|
||||
Self::Android
|
||||
} else if target.contains("ios") {
|
||||
Self::Ios
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current build target.
|
||||
pub fn current() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Self::MacOS
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else if cfg!(target_os = "ios") {
|
||||
Self::Ios
|
||||
} else if cfg!(target_os = "android") {
|
||||
Self::Android
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the target is mobile or not.
|
||||
pub fn is_mobile(&self) -> bool {
|
||||
matches!(self, Target::Android | Target::Ios)
|
||||
}
|
||||
|
||||
/// Whether the target is desktop or not.
|
||||
pub fn is_desktop(&self) -> bool {
|
||||
!self.is_mobile()
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the currently running binary's path, taking into account security considerations.
|
||||
///
|
||||
/// The path is cached as soon as possible (before even `main` runs) and that value is returned
|
||||
/// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
|
||||
/// or explicitly disabled (see following macOS specific behavior).
|
||||
///
|
||||
/// # Platform-specific behavior
|
||||
///
|
||||
/// On `macOS`, this function will return an error if the original path contained any symlinks
|
||||
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
|
||||
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
|
||||
///
|
||||
/// # Security
|
||||
///
|
||||
/// If the above platform-specific behavior does **not** take place, this function uses the
|
||||
/// following resolution.
|
||||
///
|
||||
/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
|
||||
/// This avoids the usual issue of needing the file to exist at the passed path because a valid
|
||||
/// current executable result for our purpose should always exist. Notably,
|
||||
/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
|
||||
/// hard links. Let's cover some specific topics that relate to different ways an attacker might
|
||||
/// try to trick this function into returning the wrong binary path.
|
||||
///
|
||||
/// ## Symlinks ("Soft Links")
|
||||
///
|
||||
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
|
||||
/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
|
||||
/// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
|
||||
/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
|
||||
///
|
||||
/// ## Hard Links
|
||||
///
|
||||
/// A [Hard Link] is a named entry that points to a file in the file system.
|
||||
/// On most systems, this is what you would think of as a "file". The term is
|
||||
/// used on filesystems that allow multiple entries to point to the same file.
|
||||
/// The linked [Hard Link] Wikipedia page provides a decent overview.
|
||||
///
|
||||
/// In short, unless the attacker was able to create the link with elevated
|
||||
/// permissions, it should generally not be possible for them to hard link
|
||||
/// to a file they do not have permissions to - with exception to possible
|
||||
/// operating system exploits.
|
||||
///
|
||||
/// There are also some platform-specific information about this below.
|
||||
///
|
||||
/// ### Windows
|
||||
///
|
||||
/// Windows requires a permission to be set for the user to create a symlink
|
||||
/// or a hard link, regardless of ownership status of the target. Elevated
|
||||
/// permissions users have the ability to create them.
|
||||
///
|
||||
/// ### macOS
|
||||
///
|
||||
/// macOS allows for the creation of symlinks and hard links to any file.
|
||||
/// Accessing through those links will fail if the user who owns the links
|
||||
/// does not have the proper permissions on the original file.
|
||||
///
|
||||
/// ### Linux
|
||||
///
|
||||
/// Linux allows for the creation of symlinks to any file. Accessing the
|
||||
/// symlink will fail if the user who owns the symlink does not have the
|
||||
/// proper permissions on the original file.
|
||||
///
|
||||
/// Linux additionally provides a kernel hardening feature since version
|
||||
/// 3.6 (30 September 2012). Most distributions since then have enabled
|
||||
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
|
||||
/// means that a vast majority of desktop Linux users should have it enabled.
|
||||
/// **The feature prevents the creation of hardlinks that the user does not own
|
||||
/// or have read/write access to.** [See the patch that enabled this].
|
||||
///
|
||||
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
|
||||
/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
|
||||
pub fn current_exe() -> std::io::Result<PathBuf> {
|
||||
self::starting_binary::STARTING_BINARY.cloned()
|
||||
}
|
||||
|
||||
/// Try to determine the current target triple.
|
||||
///
|
||||
/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
|
||||
/// `Error::Config` if the current config cannot be determined or is not some combination of the
|
||||
/// following values:
|
||||
/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Unexpected system config
|
||||
pub fn target_triple() -> crate::Result<String> {
|
||||
let arch = if cfg!(target_arch = "x86") {
|
||||
"i686"
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else if cfg!(target_arch = "arm") {
|
||||
"armv7"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"aarch64"
|
||||
} else if cfg!(target_arch = "riscv64") {
|
||||
"riscv64"
|
||||
} else {
|
||||
return Err(crate::Error::Architecture);
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"unknown-linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"apple-darwin"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"pc-windows"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"unknown-freebsd"
|
||||
} else {
|
||||
return Err(crate::Error::Os);
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
|
||||
String::from(os)
|
||||
} else {
|
||||
let env = if cfg!(target_env = "gnu") {
|
||||
"gnu"
|
||||
} else if cfg!(target_env = "musl") {
|
||||
"musl"
|
||||
} else if cfg!(target_env = "msvc") {
|
||||
"msvc"
|
||||
} else {
|
||||
return Err(crate::Error::Environment);
|
||||
};
|
||||
|
||||
format!("{os}-{env}")
|
||||
};
|
||||
|
||||
Ok(format!("{arch}-{os}"))
|
||||
}
|
||||
|
||||
#[cfg(all(not(test), not(target_os = "android")))]
|
||||
fn is_cargo_output_directory(path: &std::path::Path) -> bool {
|
||||
path.join(".cargo-lock").exists()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"];
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_cargo_output_directory(path: &std::path::Path) -> bool {
|
||||
let Some(last_component) = path.components().next_back() else {
|
||||
return false;
|
||||
};
|
||||
CARGO_OUTPUT_DIRECTORIES
|
||||
.iter()
|
||||
.any(|dirname| &last_component.as_os_str() == dirname)
|
||||
}
|
||||
|
||||
/// Computes the resource directory of the current environment.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows:** Resolves to the directory that contains the main executable.
|
||||
/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
|
||||
/// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
|
||||
/// If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
|
||||
/// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
|
||||
/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
|
||||
/// - **iOS:** Resolves to `${exe_dir}/assets`.
|
||||
/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
|
||||
/// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
|
||||
/// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
|
||||
/// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
|
||||
pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
|
||||
#[cfg(target_os = "android")]
|
||||
return resource_dir_android(package_info, env);
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let exe = current_exe()?;
|
||||
resource_dir_from(exe, package_info, env)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result<PathBuf> {
|
||||
Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[allow(unused_variables)]
|
||||
fn resource_dir_from<P: AsRef<std::path::Path>>(
|
||||
exe: P,
|
||||
package_info: &PackageInfo,
|
||||
env: &Env,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
|
||||
let curr_dir = exe_dir.display().to_string();
|
||||
|
||||
let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect();
|
||||
let len = parts.len();
|
||||
|
||||
// Check if running from the Cargo output directory, which means it's an executable in a development machine
|
||||
// We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
|
||||
// and see if there's a .cargo-lock file along the executable
|
||||
// This ensures the check is safer so it doesn't affect apps in production
|
||||
// Windows also includes the resources in the executable folder so we check that too
|
||||
if cfg!(target_os = "windows")
|
||||
|| ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
|
||||
&& is_cargo_output_directory(exe_dir)
|
||||
{
|
||||
return Ok(exe_dir.to_path_buf());
|
||||
}
|
||||
|
||||
#[allow(unused_mut, unused_assignments)]
|
||||
let mut res = Err(crate::Error::UnsupportedPlatform);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// (canonicalize checks for existence, so there's no need for an extra check)
|
||||
res = if let Ok(bundle_dir) = exe_dir
|
||||
.join(format!("../lib/{}", package_info.name))
|
||||
.canonicalize()
|
||||
{
|
||||
Ok(bundle_dir)
|
||||
} else if let Some(appdir) = &env.appdir {
|
||||
let appdir: &std::path::Path = appdir.as_ref();
|
||||
Ok(PathBuf::from(format!(
|
||||
"{}/usr/lib/{}",
|
||||
appdir.display(),
|
||||
package_info.name
|
||||
)))
|
||||
} else {
|
||||
// running bundle
|
||||
Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name)))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
res = exe_dir
|
||||
.join("../Resources")
|
||||
.canonicalize()
|
||||
.map_err(Into::into);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
res = exe_dir.join("assets").canonicalize().map_err(Into::into);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
// Variable holding the type of bundle the executable is stored in. This is modified by binary
|
||||
// patching during build
|
||||
#[used]
|
||||
// Marked as `mut` because it could get optimized away without it,
|
||||
// see https://github.com/tauri-apps/tauri/pull/13812
|
||||
static mut __TAURI_BUNDLE_TYPE: &str = "__TAURI_BUNDLE_TYPE_VAR_UNK";
|
||||
|
||||
/// Get the type of the bundle current binary is packaged in.
|
||||
/// If the bundle type is unknown, it returns [`Option::None`].
|
||||
pub fn bundle_type() -> Option<BundleType> {
|
||||
unsafe {
|
||||
match __TAURI_BUNDLE_TYPE {
|
||||
"__TAURI_BUNDLE_TYPE_VAR_DEB" => Some(BundleType::Deb),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_RPM" => Some(BundleType::Rpm),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_APP" => Some(BundleType::AppImage),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_MSI" => Some(BundleType::Msi),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_NSS" => Some(BundleType::Nsis),
|
||||
_ => {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(BundleType::App)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ToTokens for Target {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::platform::Target };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::MacOS => quote! { #prefix::MacOS },
|
||||
Self::Linux => quote! { #prefix::Linux },
|
||||
Self::Windows => quote! { #prefix::Windows },
|
||||
Self::Android => quote! { #prefix::Android },
|
||||
Self::Ios => quote! { #prefix::Ios },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{Env, PackageInfo};
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn resolve_resource_dir() {
|
||||
let package_info = PackageInfo {
|
||||
name: "MyApp".into(),
|
||||
version: "1.0.0".parse().unwrap(),
|
||||
authors: "",
|
||||
description: "",
|
||||
crate_name: "my-app",
|
||||
};
|
||||
let env = Env::default();
|
||||
|
||||
let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
|
||||
assert_eq!(resource_dir, path.parent().unwrap());
|
||||
|
||||
let path = PathBuf::from("/path/to/target/custom-profile/app");
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
|
||||
assert_eq!(resource_dir, path.parent().unwrap());
|
||||
|
||||
let path = PathBuf::from("/path/to/target/release/app");
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
|
||||
assert_eq!(resource_dir, path.parent().unwrap());
|
||||
|
||||
let path = PathBuf::from("/path/to/target/unknown-profile/app");
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env);
|
||||
#[cfg(target_os = "macos")]
|
||||
assert!(resource_dir.is_err());
|
||||
#[cfg(target_os = "linux")]
|
||||
assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/MyApp"));
|
||||
#[cfg(windows)]
|
||||
assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
|
||||
}
|
||||
}
|
||||
83
apps/tauri/src-tauri/vendor/tauri-utils/src/platform/starting_binary.rs
vendored
Normal file
83
apps/tauri/src-tauri/vendor/tauri-utils/src/platform/starting_binary.rs
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use ctor::ctor;
|
||||
use std::{
|
||||
io::{Error, ErrorKind, Result},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A cached version of the current binary using [`ctor`] to cache it before even `main` runs.
|
||||
#[ctor]
|
||||
#[used]
|
||||
pub(super) static STARTING_BINARY: StartingBinary = unsafe { StartingBinary::new() };
|
||||
|
||||
/// Represents a binary path that was cached when the program was loaded.
|
||||
pub(super) struct StartingBinary(std::io::Result<PathBuf>);
|
||||
|
||||
impl StartingBinary {
|
||||
/// Find the starting executable as safely as possible.
|
||||
fn new() -> Self {
|
||||
// see notes on current_exe() for security implications
|
||||
let dangerous_path = match std::env::current_exe() {
|
||||
Ok(dangerous_path) => dangerous_path,
|
||||
error @ Err(_) => return Self(error),
|
||||
};
|
||||
|
||||
// note: this only checks symlinks on problematic platforms, see implementation below
|
||||
if let Some(symlink) = Self::has_symlink(&dangerous_path) {
|
||||
return Self(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!(
|
||||
"StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}",
|
||||
symlink.display()
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
// we canonicalize the path to resolve any symlinks to the real exe path
|
||||
Self(dangerous_path.canonicalize())
|
||||
}
|
||||
|
||||
/// A clone of the [`PathBuf`] found to be the starting path.
|
||||
///
|
||||
/// Because [`Error`] is not clone-able, it is recreated instead.
|
||||
pub(super) fn cloned(&self) -> Result<PathBuf> {
|
||||
// false positive
|
||||
#[allow(clippy::useless_asref)]
|
||||
self
|
||||
.0
|
||||
.as_ref()
|
||||
.map(Clone::clone)
|
||||
.map_err(|e| Error::new(e.kind(), e.to_string()))
|
||||
}
|
||||
|
||||
/// We only care about checking this on macOS currently, as it has the least symlink protections.
|
||||
#[cfg(any(
|
||||
not(target_os = "macos"),
|
||||
feature = "process-relaunch-dangerous-allow-symlink-macos"
|
||||
))]
|
||||
fn has_symlink(_: &Path) -> Option<&Path> {
|
||||
None
|
||||
}
|
||||
|
||||
/// We only care about checking this on macOS currently, as it has the least symlink protections.
|
||||
#[cfg(all(
|
||||
target_os = "macos",
|
||||
not(feature = "process-relaunch-dangerous-allow-symlink-macos")
|
||||
))]
|
||||
fn has_symlink(path: &Path) -> Option<&Path> {
|
||||
path.ancestors().find(|ancestor| {
|
||||
matches!(
|
||||
ancestor
|
||||
.symlink_metadata()
|
||||
.as_ref()
|
||||
.map(std::fs::Metadata::file_type)
|
||||
.as_ref()
|
||||
.map(std::fs::FileType::is_symlink),
|
||||
Ok(true)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
88
apps/tauri/src-tauri/vendor/tauri-utils/src/plugin.rs
vendored
Normal file
88
apps/tauri/src-tauri/vendor/tauri-utils/src/plugin.rs
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Compile-time and runtime types for Tauri plugins.
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub use build::*;
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use std::{
|
||||
env::vars_os,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const GLOBAL_API_SCRIPT_PATH_KEY: &str = "GLOBAL_API_SCRIPT_PATH";
|
||||
/// Known file name of the file that contains an array with the path of all API scripts defined with [`define_global_api_script_path`].
|
||||
pub const GLOBAL_API_SCRIPT_FILE_LIST_PATH: &str = "__global-api-script.js";
|
||||
|
||||
/// Defines the path to the global API script using Cargo instructions.
|
||||
pub fn define_global_api_script_path(path: &Path) {
|
||||
println!(
|
||||
"cargo:{GLOBAL_API_SCRIPT_PATH_KEY}={}",
|
||||
path
|
||||
.canonicalize()
|
||||
.expect("failed to canonicalize global API script path")
|
||||
.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// Collects the path of all the global API scripts defined with [`define_global_api_script_path`]
|
||||
/// and saves them to the out dir with filename [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`].
|
||||
///
|
||||
/// `tauri_global_scripts` is only used in Tauri's monorepo for the examples to work
|
||||
/// since they don't have a build script to run `tauri-build` and pull in the deps env vars
|
||||
pub fn save_global_api_scripts_paths(out_dir: &Path, mut tauri_global_scripts: Option<PathBuf>) {
|
||||
let mut scripts = Vec::new();
|
||||
|
||||
for (key, value) in vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
|
||||
if key == format!("DEP_TAURI_{GLOBAL_API_SCRIPT_PATH_KEY}") {
|
||||
tauri_global_scripts = Some(PathBuf::from(value));
|
||||
} else if key.starts_with("DEP_") && key.ends_with(GLOBAL_API_SCRIPT_PATH_KEY) {
|
||||
let script_path = PathBuf::from(value);
|
||||
scripts.push(script_path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tauri_global_scripts) = tauri_global_scripts {
|
||||
scripts.insert(0, tauri_global_scripts);
|
||||
}
|
||||
|
||||
fs::write(
|
||||
out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH),
|
||||
serde_json::to_string(&scripts).expect("failed to serialize global API script paths"),
|
||||
)
|
||||
.expect("failed to write global API script");
|
||||
}
|
||||
|
||||
/// Read global api scripts from [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`]
|
||||
pub fn read_global_api_scripts(out_dir: &Path) -> Option<Vec<String>> {
|
||||
let global_scripts_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH);
|
||||
if !global_scripts_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let global_scripts_str = fs::read_to_string(global_scripts_path)
|
||||
.expect("failed to read plugin global API script paths");
|
||||
let global_scripts = serde_json::from_str::<Vec<PathBuf>>(&global_scripts_str)
|
||||
.expect("failed to parse plugin global API script paths");
|
||||
|
||||
Some(
|
||||
global_scripts
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
fs::read_to_string(&p).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"failed to read plugin global API script {}: {e}",
|
||||
p.display()
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
643
apps/tauri/src-tauri/vendor/tauri-utils/src/resources.rs
vendored
Normal file
643
apps/tauri/src-tauri/vendor/tauri-utils/src/resources.rs
vendored
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::platform::Target as TargetPlatform;
|
||||
|
||||
/// Given a path (absolute or relative) to a resource file, returns the
|
||||
/// relative path from the bundle resources directory where that resource
|
||||
/// should be stored.
|
||||
pub fn resource_relpath(path: &Path) -> PathBuf {
|
||||
let mut dest = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(_) => {}
|
||||
Component::RootDir => dest.push("_root_"),
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => dest.push("_up_"),
|
||||
Component::Normal(string) => dest.push(string),
|
||||
}
|
||||
}
|
||||
dest
|
||||
}
|
||||
|
||||
fn normalize(path: &Path) -> PathBuf {
|
||||
let mut dest = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(_) => {}
|
||||
Component::RootDir => dest.push("/"),
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => dest.push(".."),
|
||||
Component::Normal(string) => dest.push(string),
|
||||
}
|
||||
}
|
||||
dest
|
||||
}
|
||||
|
||||
/// Parses the external binaries to bundle, adding the target triple suffix to each of them.
|
||||
pub fn external_binaries(
|
||||
external_binaries: &[String],
|
||||
target_triple: &str,
|
||||
target_platform: &TargetPlatform,
|
||||
) -> Vec<String> {
|
||||
let mut paths = Vec::new();
|
||||
for curr_path in external_binaries {
|
||||
let extension = if matches!(target_platform, TargetPlatform::Windows) {
|
||||
".exe"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
paths.push(format!("{curr_path}-{target_triple}{extension}"));
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
/// Information for a resource.
|
||||
#[derive(Debug)]
|
||||
pub struct Resource {
|
||||
path: PathBuf,
|
||||
target: PathBuf,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
/// The path of the resource.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// The target location of the resource.
|
||||
pub fn target(&self) -> &Path {
|
||||
&self.target
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PatternIter<'a> {
|
||||
Slice(std::slice::Iter<'a, String>),
|
||||
Map(std::collections::hash_map::Iter<'a, String, String>),
|
||||
}
|
||||
|
||||
/// A helper to iterate through resources.
|
||||
pub struct ResourcePaths<'a> {
|
||||
iter: ResourcePathsIter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ResourcePaths<'a> {
|
||||
/// Creates a new ResourcePaths from a slice of patterns to iterate
|
||||
pub fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> {
|
||||
ResourcePaths {
|
||||
iter: ResourcePathsIter {
|
||||
pattern_iter: PatternIter::Slice(patterns.iter()),
|
||||
allow_walk,
|
||||
current_dest: None,
|
||||
current_iter: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new ResourcePaths from a slice of patterns to iterate
|
||||
pub fn from_map(patterns: &'a HashMap<String, String>, allow_walk: bool) -> ResourcePaths<'a> {
|
||||
ResourcePaths {
|
||||
iter: ResourcePathsIter {
|
||||
pattern_iter: PatternIter::Map(patterns.iter()),
|
||||
allow_walk,
|
||||
current_dest: None,
|
||||
current_iter: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the resource iterator that yields the source and target paths.
|
||||
/// Needed when using [`Self::from_map`].
|
||||
pub fn iter(self) -> ResourcePathsIter<'a> {
|
||||
self.iter
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator of a [`ResourcePaths`].
|
||||
#[derive(Debug)]
|
||||
pub struct ResourcePathsIter<'a> {
|
||||
/// the patterns to iterate.
|
||||
pattern_iter: PatternIter<'a>,
|
||||
/// whether the resource paths allows directories or not.
|
||||
allow_walk: bool,
|
||||
|
||||
/// The value of map when [`Self::pattern_iter`] is a [`PatternIter::Map`],
|
||||
/// used for determining [`Resource::target`]
|
||||
current_dest: Option<PathBuf>,
|
||||
/// The iter for the current pattern. The cycle goes like this:
|
||||
/// [`ResourcePaths::next`] -> [`Self::next`] -> [`Self::pattern_iter::next`] -> [`Self::current_iter::next`]
|
||||
current_iter: Option<ResourcePathsInnerIter>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ResourcePathsInnerIter {
|
||||
Walk {
|
||||
iter: walkdir::IntoIter,
|
||||
/// The key of map when [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Map`],
|
||||
/// used for determining [`Resource::target`]
|
||||
current_pattern: Option<PathBuf>,
|
||||
},
|
||||
Glob {
|
||||
iter: glob::Paths,
|
||||
},
|
||||
}
|
||||
|
||||
impl Iterator for ResourcePathsInnerIter {
|
||||
type Item = crate::Result<PathBuf>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
|
||||
match self {
|
||||
ResourcePathsInnerIter::Walk { iter, .. } => Some(
|
||||
iter
|
||||
.next()?
|
||||
.map(|entry| entry.into_path())
|
||||
.map_err(Into::into),
|
||||
),
|
||||
ResourcePathsInnerIter::Glob { iter } => Some(iter.next()?.map_err(Into::into)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourcePathsIter<'_> {
|
||||
fn next_current_iter(&mut self) -> Option<crate::Result<Resource>> {
|
||||
let current_iter = self.current_iter.as_mut().unwrap();
|
||||
let entry = current_iter.next()?;
|
||||
|
||||
Some(match entry {
|
||||
Ok(entry) => {
|
||||
// Skip directories
|
||||
if entry.is_dir() {
|
||||
self.next_current_iter()?
|
||||
} else {
|
||||
self.resource_from_path(normalize(&entry))
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
})
|
||||
}
|
||||
|
||||
fn resource_from_path(&self, path: PathBuf) -> crate::Result<Resource> {
|
||||
if !path.exists() {
|
||||
return Err(crate::Error::ResourcePathNotFound(path));
|
||||
}
|
||||
|
||||
Ok(Resource {
|
||||
target: if let Some(dest) = &self.current_dest {
|
||||
match &self.current_iter {
|
||||
Some(current_iter) => match current_iter {
|
||||
// if processing a directory, preserve directory structure under current_dest
|
||||
ResourcePathsInnerIter::Walk {
|
||||
current_pattern, ..
|
||||
} => {
|
||||
if let Some(pattern) = current_pattern {
|
||||
dest.join(path.strip_prefix(pattern).unwrap_or(&path))
|
||||
} else {
|
||||
dest.join(&path)
|
||||
}
|
||||
}
|
||||
// if processing a glob and current_dest is not empty
|
||||
// we put all globbed paths under current_dest
|
||||
// preserving the file name as it is
|
||||
ResourcePathsInnerIter::Glob { .. } => dest.join(path.file_name().unwrap()),
|
||||
},
|
||||
None => dest.clone(),
|
||||
}
|
||||
} else {
|
||||
// If [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Slice`]
|
||||
resource_relpath(&path)
|
||||
},
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
fn next_pattern(&mut self) -> Option<crate::Result<Resource>> {
|
||||
self.current_dest = None;
|
||||
|
||||
let pattern = match &mut self.pattern_iter {
|
||||
PatternIter::Slice(iter) => iter.next()?,
|
||||
PatternIter::Map(iter) => {
|
||||
let (pattern, dest) = iter.next()?;
|
||||
self.current_dest = Some(resource_relpath(Path::new(dest)));
|
||||
pattern
|
||||
}
|
||||
};
|
||||
|
||||
if pattern.contains('*') {
|
||||
self.current_iter = match glob::glob(pattern) {
|
||||
Ok(glob) => Some(ResourcePathsInnerIter::Glob { iter: glob }),
|
||||
Err(error) => return Some(Err(error.into())),
|
||||
};
|
||||
match self.next_current_iter() {
|
||||
Some(r) => return Some(r),
|
||||
None => {
|
||||
self.current_iter = None;
|
||||
return Some(Err(crate::Error::GlobPathNotFound(pattern.clone())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let path = normalize(Path::new(pattern));
|
||||
if path.is_dir() {
|
||||
if !self.allow_walk {
|
||||
return Some(Err(crate::Error::NotAllowedToWalkDir(path)));
|
||||
}
|
||||
self.current_iter = Some(ResourcePathsInnerIter::Walk {
|
||||
iter: WalkDir::new(&path).into_iter(),
|
||||
current_pattern: if matches!(self.pattern_iter, PatternIter::Map(_)) {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return Some(self.resource_from_path(path));
|
||||
}
|
||||
}
|
||||
|
||||
self.next_current_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ResourcePaths<'_> {
|
||||
type Item = crate::Result<PathBuf>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
|
||||
self.iter.next().map(|r| r.map(|res| res.path))
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ResourcePathsIter<'_> {
|
||||
type Item = crate::Result<Resource>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<Resource>> {
|
||||
if self.current_iter.is_some() {
|
||||
match self.next_current_iter() {
|
||||
Some(r) => return Some(r),
|
||||
None => self.current_iter = None,
|
||||
}
|
||||
}
|
||||
|
||||
self.next_pattern()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
impl PartialEq for Resource {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path && self.target == other.target
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_resources(resources: &[(&str, &str)]) -> Vec<Resource> {
|
||||
resources
|
||||
.iter()
|
||||
.map(|(path, target)| Resource {
|
||||
path: Path::new(path).components().collect(),
|
||||
target: Path::new(target).components().collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn setup_test_dirs() {
|
||||
let mut random = [0; 1];
|
||||
getrandom::fill(&mut random).unwrap();
|
||||
|
||||
let temp = std::env::temp_dir();
|
||||
let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0]));
|
||||
|
||||
let _ = fs::remove_dir_all(&temp);
|
||||
fs::create_dir_all(&temp).unwrap();
|
||||
|
||||
std::env::set_current_dir(&temp).unwrap();
|
||||
|
||||
let paths = [
|
||||
"src-tauri/tauri.conf.json",
|
||||
"src-tauri/some-other-json.json",
|
||||
"src-tauri/Cargo.toml",
|
||||
"src-tauri/Tauri.toml",
|
||||
"src-tauri/build.rs",
|
||||
"src-tauri/some-folder/some-file.txt",
|
||||
"src/assets/javascript.svg",
|
||||
"src/assets/tauri.svg",
|
||||
"src/assets/rust.svg",
|
||||
"src/assets/lang/en.json",
|
||||
"src/assets/lang/ar.json",
|
||||
"src/sounds/lang/es.wav",
|
||||
"src/sounds/lang/fr.wav",
|
||||
"src/textures/ground/earth.tex",
|
||||
"src/textures/ground/sand.tex",
|
||||
"src/textures/water.tex",
|
||||
"src/textures/fire.tex",
|
||||
"src/tiles/sky/grey.tile",
|
||||
"src/tiles/sky/yellow.tile",
|
||||
"src/tiles/grass.tile",
|
||||
"src/tiles/stones.tile",
|
||||
"src/index.html",
|
||||
"src/style.css",
|
||||
"src/script.js",
|
||||
"src/dir/another-dir/file1.txt",
|
||||
"src/dir/another-dir2/file2.txt",
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
let path = Path::new(path);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, "").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn resources_map(literal: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
literal
|
||||
.iter()
|
||||
.map(|(from, to)| (from.to_string(), to.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_slice_allow_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::new(
|
||||
&[
|
||||
"../src/script.js".into(),
|
||||
"../src/assets".into(),
|
||||
"../src/index.html".into(),
|
||||
"../src/sounds".into(),
|
||||
// Should be the same as `../src/textures/` or `../src/textures`
|
||||
"../src/textures/**/*".into(),
|
||||
"*.toml".into(),
|
||||
"*.conf.json".into(),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
// From `../src/script.js`
|
||||
("../src/script.js", "_up_/src/script.js"),
|
||||
// From `../src/assets`
|
||||
(
|
||||
"../src/assets/javascript.svg",
|
||||
"_up_/src/assets/javascript.svg",
|
||||
),
|
||||
("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"),
|
||||
("../src/assets/rust.svg", "_up_/src/assets/rust.svg"),
|
||||
("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"),
|
||||
("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"),
|
||||
// From `../src/index.html`
|
||||
("../src/index.html", "_up_/src/index.html"),
|
||||
// From `../src/sounds`
|
||||
("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"),
|
||||
("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"),
|
||||
// From `../src/textures/**/*`
|
||||
(
|
||||
"../src/textures/ground/earth.tex",
|
||||
"_up_/src/textures/ground/earth.tex",
|
||||
),
|
||||
(
|
||||
"../src/textures/ground/sand.tex",
|
||||
"_up_/src/textures/ground/sand.tex",
|
||||
),
|
||||
("../src/textures/water.tex", "_up_/src/textures/water.tex"),
|
||||
("../src/textures/fire.tex", "_up_/src/textures/fire.tex"),
|
||||
// From `*.toml`
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
// From `*.conf.json`
|
||||
("tauri.conf.json", "tauri.conf.json"),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_slice_no_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::new(
|
||||
&[
|
||||
"../src/script.js".into(),
|
||||
"../src/assets".into(),
|
||||
"../src/index.html".into(),
|
||||
"../src/sounds".into(),
|
||||
"*.toml".into(),
|
||||
"*.conf.json".into(),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
("../src/script.js", "_up_/src/script.js"),
|
||||
("../src/index.html", "_up_/src/index.html"),
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
("tauri.conf.json", "tauri.conf.json"),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_map_allow_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::from_map(
|
||||
&resources_map(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/assets", ""),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("../src/sounds", "voices"),
|
||||
("../src/textures/*", "textures"),
|
||||
("../src/tiles/**/*", "tiles"),
|
||||
("*.toml", ""),
|
||||
("*.conf.json", "json"),
|
||||
("./some-folder/", "some-target-folder/"),
|
||||
("../non-existent-file", "asd"), // invalid case
|
||||
("../non/*", "asd"), // invalid case
|
||||
]),
|
||||
true,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/assets/javascript.svg", "javascript.svg"),
|
||||
("../src/assets/tauri.svg", "tauri.svg"),
|
||||
("../src/assets/rust.svg", "rust.svg"),
|
||||
("../src/assets/lang/en.json", "lang/en.json"),
|
||||
("../src/assets/lang/ar.json", "lang/ar.json"),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("../src/sounds/lang/es.wav", "voices/lang/es.wav"),
|
||||
("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"),
|
||||
("../src/textures/water.tex", "textures/water.tex"),
|
||||
("../src/textures/fire.tex", "textures/fire.tex"),
|
||||
("../src/tiles/grass.tile", "tiles/grass.tile"),
|
||||
("../src/tiles/stones.tile", "tiles/stones.tile"),
|
||||
("../src/tiles/sky/grey.tile", "tiles/grey.tile"),
|
||||
("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"),
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
("tauri.conf.json", "json/tauri.conf.json"),
|
||||
(
|
||||
"some-folder/some-file.txt",
|
||||
"some-target-folder/some-file.txt",
|
||||
),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_map_no_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::from_map(
|
||||
&resources_map(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/assets", ""),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("../src/sounds", "voices"),
|
||||
("*.toml", ""),
|
||||
("*.conf.json", "json"),
|
||||
]),
|
||||
false,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
("tauri.conf.json", "json/tauri.conf.json"),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_errors() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::from_map(
|
||||
&resources_map(&[
|
||||
("../non-existent-file", "file"),
|
||||
("../non-existent-dir", "dir"),
|
||||
// exists but not allowed to walk
|
||||
("../src", "dir2"),
|
||||
// doesn't exist but it is a glob and will return an error
|
||||
("../non-existent-glob-dir/*", "glob"),
|
||||
// exists but only contains directories and will not produce any values
|
||||
("../src/dir/*", "dir3"),
|
||||
]),
|
||||
false,
|
||||
)
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(resources.len(), 5);
|
||||
|
||||
assert!(resources.iter().all(|r| r.is_err()));
|
||||
|
||||
// hashmap order is not guaranteed so we check the error variant exists and how many
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
175
apps/tauri/src-tauri/vendor/tauri-utils/src/tokens.rs
vendored
Normal file
175
apps/tauri/src-tauri/vendor/tauri-utils/src/tokens.rs
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Utilities to implement [`ToTokens`] for a type.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use serde_json::Value as JsonValue;
|
||||
use url::Url;
|
||||
|
||||
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
|
||||
///
|
||||
/// All fields must represent a binding of the same name that implements `ToTokens`.
|
||||
#[macro_export]
|
||||
macro_rules! literal_struct {
|
||||
($tokens:ident, $struct:path, $($field:ident),+) => {
|
||||
$tokens.append_all(quote! {
|
||||
$struct {
|
||||
$($field: #$field),+
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a `String` constructor `TokenStream`.
|
||||
///
|
||||
/// e.g. `"Hello World"` -> `String::from("Hello World")`.
|
||||
/// This takes a `&String` to reduce casting all the `&String` -> `&str` manually.
|
||||
pub fn str_lit(s: impl AsRef<str>) -> TokenStream {
|
||||
let s = s.as_ref();
|
||||
quote! { #s.into() }
|
||||
}
|
||||
|
||||
/// Create an `Option` constructor `TokenStream`.
|
||||
pub fn opt_lit(item: Option<&impl ToTokens>) -> TokenStream {
|
||||
match item {
|
||||
None => quote! { ::core::option::Option::None },
|
||||
Some(item) => quote! { ::core::option::Option::Some(#item) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an `Option` constructor `TokenStream` over an owned [`ToTokens`] impl type.
|
||||
pub fn opt_lit_owned(item: Option<impl ToTokens>) -> TokenStream {
|
||||
match item {
|
||||
None => quote! { ::core::option::Option::None },
|
||||
Some(item) => quote! { ::core::option::Option::Some(#item) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to combine an `opt_lit` with `str_lit`.
|
||||
pub fn opt_str_lit(item: Option<impl AsRef<str>>) -> TokenStream {
|
||||
opt_lit(item.map(str_lit).as_ref())
|
||||
}
|
||||
|
||||
/// Helper function to combine an `opt_lit` with a list of `str_lit`
|
||||
pub fn opt_vec_lit<Raw, Tokens>(
|
||||
item: Option<impl IntoIterator<Item = Raw>>,
|
||||
map: impl Fn(Raw) -> Tokens,
|
||||
) -> TokenStream
|
||||
where
|
||||
Tokens: ToTokens,
|
||||
{
|
||||
opt_lit(item.map(|list| vec_lit(list, map)).as_ref())
|
||||
}
|
||||
|
||||
/// Create a `Vec` constructor, mapping items with a function that spits out `TokenStream`s.
|
||||
pub fn vec_lit<Raw, Tokens>(
|
||||
list: impl IntoIterator<Item = Raw>,
|
||||
map: impl Fn(Raw) -> Tokens,
|
||||
) -> TokenStream
|
||||
where
|
||||
Tokens: ToTokens,
|
||||
{
|
||||
let items = list.into_iter().map(map);
|
||||
quote! { vec![#(#items),*] }
|
||||
}
|
||||
|
||||
/// Create a `PathBuf` constructor `TokenStream`.
|
||||
///
|
||||
/// e.g. `"Hello World" -> String::from("Hello World").
|
||||
pub fn path_buf_lit(s: impl AsRef<Path>) -> TokenStream {
|
||||
let s = s.as_ref().to_string_lossy().into_owned();
|
||||
quote! { ::std::path::PathBuf::from(#s) }
|
||||
}
|
||||
|
||||
/// Creates a `Url` constructor `TokenStream`.
|
||||
pub fn url_lit(url: &Url) -> TokenStream {
|
||||
let url = url.as_str();
|
||||
quote! { #url.parse().unwrap() }
|
||||
}
|
||||
|
||||
/// Create a map constructor, mapping keys and values with other `TokenStream`s.
|
||||
///
|
||||
/// This function is pretty generic because the types of keys AND values get transformed.
|
||||
pub fn map_lit<Map, Key, Value, TokenStreamKey, TokenStreamValue, FuncKey, FuncValue>(
|
||||
map_type: TokenStream,
|
||||
map: Map,
|
||||
map_key: FuncKey,
|
||||
map_value: FuncValue,
|
||||
) -> TokenStream
|
||||
where
|
||||
<Map as IntoIterator>::IntoIter: ExactSizeIterator,
|
||||
Map: IntoIterator<Item = (Key, Value)>,
|
||||
TokenStreamKey: ToTokens,
|
||||
TokenStreamValue: ToTokens,
|
||||
FuncKey: Fn(Key) -> TokenStreamKey,
|
||||
FuncValue: Fn(Value) -> TokenStreamValue,
|
||||
{
|
||||
let ident = quote::format_ident!("map");
|
||||
let map = map.into_iter();
|
||||
|
||||
if map.len() > 0 {
|
||||
let items = map.map(|(key, value)| {
|
||||
let key = map_key(key);
|
||||
let value = map_value(value);
|
||||
quote! { #ident.insert(#key, #value); }
|
||||
});
|
||||
|
||||
quote! {{
|
||||
let mut #ident = #map_type::new();
|
||||
#(#items)*
|
||||
#ident
|
||||
}}
|
||||
} else {
|
||||
quote! { #map_type::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `serde_json::Value` variant `TokenStream` for a number
|
||||
pub fn json_value_number_lit(num: &serde_json::Number) -> TokenStream {
|
||||
// See <https://docs.rs/serde_json/1/serde_json/struct.Number.html> for guarantees
|
||||
let prefix = quote! { ::serde_json::Value };
|
||||
if num.is_u64() {
|
||||
// guaranteed u64
|
||||
let num = num.as_u64().unwrap();
|
||||
quote! { #prefix::Number(#num.into()) }
|
||||
} else if num.is_i64() {
|
||||
// guaranteed i64
|
||||
let num = num.as_i64().unwrap();
|
||||
quote! { #prefix::Number(#num.into()) }
|
||||
} else if num.is_f64() {
|
||||
// guaranteed f64
|
||||
let num = num.as_f64().unwrap();
|
||||
quote! { #prefix::Number(::serde_json::Number::from_f64(#num).unwrap(/* safe to unwrap, guaranteed f64 */)) }
|
||||
} else {
|
||||
// invalid number
|
||||
quote! { #prefix::Null }
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `serde_json::Value` constructor `TokenStream`
|
||||
pub fn json_value_lit(jv: &JsonValue) -> TokenStream {
|
||||
let prefix = quote! { ::serde_json::Value };
|
||||
|
||||
match jv {
|
||||
JsonValue::Null => quote! { #prefix::Null },
|
||||
JsonValue::Bool(bool) => quote! { #prefix::Bool(#bool) },
|
||||
JsonValue::Number(number) => json_value_number_lit(number),
|
||||
JsonValue::String(str) => {
|
||||
let s = str_lit(str);
|
||||
quote! { #prefix::String(#s) }
|
||||
}
|
||||
JsonValue::Array(vec) => {
|
||||
let items = vec.iter().map(json_value_lit);
|
||||
quote! { #prefix::Array(vec![#(#items),*]) }
|
||||
}
|
||||
JsonValue::Object(map) => {
|
||||
let map = map_lit(quote! { ::serde_json::Map }, map, str_lit, json_value_lit);
|
||||
quote! { #prefix::Object(#map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue