tauri_utils/assets.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
// 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},
};
/// 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.
#[derive(Debug)]
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>]>,
}
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]>> {
self
.assets
.get(key.as_ref())
.map(|&(mut asdf)| {
// 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).map(|()| buf)
})
.and_then(Result::ok)
.map(Cow::Owned)
}
/// Get an asset by key.
#[cfg(not(feature = "compression"))]
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
self
.assets
.get(key.as_ref())
.copied()
.map(|a| Cow::Owned(a.to_vec()))
}
/// 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(),
)
}
}