Skip to main content

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    let path = path.as_ref();
47
48    // add in root to mimic how it is used from a server url
49    let path = if path.has_root() {
50      Cow::Borrowed(path)
51    } else {
52      Cow::Owned(Path::new(&Component::RootDir).join(path))
53    };
54
55    let buf = if cfg!(windows) {
56      let mut buf = String::new();
57      for component in path.components() {
58        match component {
59          Component::RootDir => buf.push('/'),
60          Component::CurDir => buf.push_str("./"),
61          Component::ParentDir => buf.push_str("../"),
62          Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
63          Component::Normal(s) => {
64            buf.push_str(&s.to_string_lossy());
65            buf.push('/')
66          }
67        }
68      }
69
70      // remove the last slash
71      if buf != "/" {
72        buf.pop();
73      }
74
75      buf
76    } else {
77      path.to_string_lossy().to_string()
78    };
79
80    AssetKey(buf)
81  }
82}
83
84/// A Content-Security-Policy hash value for a specific directive.
85/// For more information see [the MDN page](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives).
86#[non_exhaustive]
87#[derive(Debug, Clone, Copy)]
88pub enum CspHash<'a> {
89  /// The `script-src` directive.
90  Script(&'a str),
91
92  /// The `style-src` directive.
93  Style(&'a str),
94}
95
96impl CspHash<'_> {
97  /// The Content-Security-Policy directive this hash applies to.
98  pub fn directive(&self) -> &'static str {
99    match self {
100      Self::Script(_) => "script-src",
101      Self::Style(_) => "style-src",
102    }
103  }
104
105  /// The value of the Content-Security-Policy hash.
106  pub fn hash(&self) -> &str {
107    match self {
108      Self::Script(hash) => hash,
109      Self::Style(hash) => hash,
110    }
111  }
112}
113
114/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
115pub struct EmbeddedAssets {
116  assets: phf::Map<&'static str, &'static [u8]>,
117  // Hashes that must be injected to the CSP of every HTML file.
118  global_hashes: &'static [CspHash<'static>],
119  // Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
120  html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
121}
122
123/// Temporary struct that overrides the Debug formatting for the `assets` field.
124///
125/// It reduces the output size compared to the default, as that would format the binary
126/// data as a slice of numbers like `[65, 66, 67]` for "ABC". This instead shows the length
127/// of the slice.
128///
129/// For example: `{"/index.html": [u8; 1835], "/index.js": [u8; 212]}`
130struct DebugAssetMap<'a>(&'a phf::Map<&'static str, &'static [u8]>);
131
132impl std::fmt::Debug for DebugAssetMap<'_> {
133  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134    let mut map = f.debug_map();
135    for (k, v) in self.0.entries() {
136      map.key(k);
137      map.value(&format_args!("[u8; {}]", v.len()));
138    }
139    map.finish()
140  }
141}
142
143impl std::fmt::Debug for EmbeddedAssets {
144  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145    f.debug_struct("EmbeddedAssets")
146      .field("assets", &DebugAssetMap(&self.assets))
147      .field("global_hashes", &self.global_hashes)
148      .field("html_hashes", &self.html_hashes)
149      .finish()
150  }
151}
152
153impl EmbeddedAssets {
154  /// Creates a new instance from the given asset map and script hash list.
155  pub const fn new(
156    map: phf::Map<&'static str, &'static [u8]>,
157    global_hashes: &'static [CspHash<'static>],
158    html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
159  ) -> Self {
160    Self {
161      assets: map,
162      global_hashes,
163      html_hashes,
164    }
165  }
166
167  /// Get an asset by key.
168  #[cfg(feature = "compression")]
169  pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
170    let &(mut asdf) = self.assets.get(key.as_ref())?;
171    // with the exception of extremely small files, output should usually be
172    // at least as large as the compressed version.
173    let mut buf = Vec::with_capacity(asdf.len());
174    brotli::BrotliDecompress(&mut asdf, &mut buf).ok()?;
175    Some(Cow::Owned(buf))
176  }
177
178  /// Get an asset by key.
179  #[cfg(not(feature = "compression"))]
180  pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
181    Some(Cow::Borrowed(self.assets.get(key.as_ref())?))
182  }
183
184  /// Iterate on the assets.
185  pub fn iter(&self) -> Box<AssetsIter<'_>> {
186    Box::new(
187      self
188        .assets
189        .into_iter()
190        .map(|(k, b)| (Cow::Borrowed(*k), Cow::Borrowed(*b))),
191    )
192  }
193
194  /// CSP hashes for the given asset.
195  pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
196    Box::new(
197      self
198        .global_hashes
199        .iter()
200        .chain(
201          self
202            .html_hashes
203            .get(html_path.as_ref())
204            .copied()
205            .into_iter()
206            .flatten(),
207        )
208        .copied(),
209    )
210  }
211}