tauri_utils/
assets.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! The Assets module allows you to read files that have been bundled by tauri
6//! during both compile time and runtime.
7
8#[doc(hidden)]
9pub use phf;
10use std::{
11  borrow::Cow,
12  path::{Component, Path},
13};
14
15/// The token used for script nonces.
16pub const SCRIPT_NONCE_TOKEN: &str = "__TAURI_SCRIPT_NONCE__";
17/// The token used for style nonces.
18pub const STYLE_NONCE_TOKEN: &str = "__TAURI_STYLE_NONCE__";
19
20/// Assets iterator.
21pub type AssetsIter<'a> = dyn Iterator<Item = (Cow<'a, str>, Cow<'a, [u8]>)> + 'a;
22
23/// Represent an asset file path in a normalized way.
24///
25/// The following rules are enforced and added if needed:
26/// * Unix path component separators
27/// * Has a root directory
28/// * No trailing slash - directories are not included in assets
29#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
30pub struct AssetKey(String);
31
32impl From<AssetKey> for String {
33  fn from(key: AssetKey) -> Self {
34    key.0
35  }
36}
37
38impl AsRef<str> for AssetKey {
39  fn as_ref(&self) -> &str {
40    &self.0
41  }
42}
43
44impl<P: AsRef<Path>> From<P> for AssetKey {
45  fn from(path: P) -> Self {
46    // TODO: change this to utilize `Cow` to prevent allocating an intermediate `PathBuf` when not necessary
47    let path = path.as_ref().to_owned();
48
49    // add in root to mimic how it is used from a server url
50    let path = if path.has_root() {
51      path
52    } else {
53      Path::new(&Component::RootDir).join(path)
54    };
55
56    let buf = if cfg!(windows) {
57      let mut buf = String::new();
58      for component in path.components() {
59        match component {
60          Component::RootDir => buf.push('/'),
61          Component::CurDir => buf.push_str("./"),
62          Component::ParentDir => buf.push_str("../"),
63          Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
64          Component::Normal(s) => {
65            buf.push_str(&s.to_string_lossy());
66            buf.push('/')
67          }
68        }
69      }
70
71      // remove the last slash
72      if buf != "/" {
73        buf.pop();
74      }
75
76      buf
77    } else {
78      path.to_string_lossy().to_string()
79    };
80
81    AssetKey(buf)
82  }
83}
84
85/// A Content-Security-Policy hash value for a specific directive.
86/// For more information see [the MDN page](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives).
87#[non_exhaustive]
88#[derive(Debug, Clone, Copy)]
89pub enum CspHash<'a> {
90  /// The `script-src` directive.
91  Script(&'a str),
92
93  /// The `style-src` directive.
94  Style(&'a str),
95}
96
97impl CspHash<'_> {
98  /// The Content-Security-Policy directive this hash applies to.
99  pub fn directive(&self) -> &'static str {
100    match self {
101      Self::Script(_) => "script-src",
102      Self::Style(_) => "style-src",
103    }
104  }
105
106  /// The value of the Content-Security-Policy hash.
107  pub fn hash(&self) -> &str {
108    match self {
109      Self::Script(hash) => hash,
110      Self::Style(hash) => hash,
111    }
112  }
113}
114
115/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
116pub struct EmbeddedAssets {
117  assets: phf::Map<&'static str, &'static [u8]>,
118  // Hashes that must be injected to the CSP of every HTML file.
119  global_hashes: &'static [CspHash<'static>],
120  // Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
121  html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
122}
123
124/// Temporary struct that overrides the Debug formatting for the `assets` field.
125///
126/// It reduces the output size compared to the default, as that would format the binary
127/// data as a slice of numbers like `[65, 66, 67]` for "ABC". This instead shows the length
128/// of the slice.
129///
130/// For example: `{"/index.html": [u8; 1835], "/index.js": [u8; 212]}`
131struct DebugAssetMap<'a>(&'a phf::Map<&'static str, &'static [u8]>);
132
133impl std::fmt::Debug for DebugAssetMap<'_> {
134  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135    let mut map = f.debug_map();
136    for (k, v) in self.0.entries() {
137      map.key(k);
138      map.value(&format_args!("[u8; {}]", v.len()));
139    }
140    map.finish()
141  }
142}
143
144impl std::fmt::Debug for EmbeddedAssets {
145  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146    f.debug_struct("EmbeddedAssets")
147      .field("assets", &DebugAssetMap(&self.assets))
148      .field("global_hashes", &self.global_hashes)
149      .field("html_hashes", &self.html_hashes)
150      .finish()
151  }
152}
153
154impl EmbeddedAssets {
155  /// Creates a new instance from the given asset map and script hash list.
156  pub const fn new(
157    map: phf::Map<&'static str, &'static [u8]>,
158    global_hashes: &'static [CspHash<'static>],
159    html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
160  ) -> Self {
161    Self {
162      assets: map,
163      global_hashes,
164      html_hashes,
165    }
166  }
167
168  /// Get an asset by key.
169  #[cfg(feature = "compression")]
170  pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
171    self
172      .assets
173      .get(key.as_ref())
174      .map(|&(mut asdf)| {
175        // with the exception of extremely small files, output should usually be
176        // at least as large as the compressed version.
177        let mut buf = Vec::with_capacity(asdf.len());
178        brotli::BrotliDecompress(&mut asdf, &mut buf).map(|()| buf)
179      })
180      .and_then(Result::ok)
181      .map(Cow::Owned)
182  }
183
184  /// Get an asset by key.
185  #[cfg(not(feature = "compression"))]
186  pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
187    self
188      .assets
189      .get(key.as_ref())
190      .copied()
191      .map(|a| Cow::Owned(a.to_vec()))
192  }
193
194  /// Iterate on the assets.
195  pub fn iter(&self) -> Box<AssetsIter<'_>> {
196    Box::new(
197      self
198        .assets
199        .into_iter()
200        .map(|(k, b)| (Cow::Borrowed(*k), Cow::Borrowed(*b))),
201    )
202  }
203
204  /// CSP hashes for the given asset.
205  pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
206    Box::new(
207      self
208        .global_hashes
209        .iter()
210        .chain(
211          self
212            .html_hashes
213            .get(html_path.as_ref())
214            .copied()
215            .into_iter()
216            .flatten(),
217        )
218        .copied(),
219    )
220  }
221}