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/// Assets iterator.
16pub type AssetsIter<'a> = dyn Iterator<Item = (Cow<'a, str>, Cow<'a, [u8]>)> + 'a;
17
18/// Represent an asset file path in a normalized way.
19///
20/// The following rules are enforced and added if needed:
21/// * Unix path component separators
22/// * Has a root directory
23/// * No trailing slash - directories are not included in assets
24#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
25pub struct AssetKey(String);
26
27impl From<AssetKey> for String {
28  fn from(key: AssetKey) -> Self {
29    key.0
30  }
31}
32
33impl AsRef<str> for AssetKey {
34  fn as_ref(&self) -> &str {
35    &self.0
36  }
37}
38
39impl<P: AsRef<Path>> From<P> for AssetKey {
40  fn from(path: P) -> Self {
41    // TODO: change this to utilize `Cow` to prevent allocating an intermediate `PathBuf` when not necessary
42    let path = path.as_ref().to_owned();
43
44    // add in root to mimic how it is used from a server url
45    let path = if path.has_root() {
46      path
47    } else {
48      Path::new(&Component::RootDir).join(path)
49    };
50
51    let buf = if cfg!(windows) {
52      let mut buf = String::new();
53      for component in path.components() {
54        match component {
55          Component::RootDir => buf.push('/'),
56          Component::CurDir => buf.push_str("./"),
57          Component::ParentDir => buf.push_str("../"),
58          Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
59          Component::Normal(s) => {
60            buf.push_str(&s.to_string_lossy());
61            buf.push('/')
62          }
63        }
64      }
65
66      // remove the last slash
67      if buf != "/" {
68        buf.pop();
69      }
70
71      buf
72    } else {
73      path.to_string_lossy().to_string()
74    };
75
76    AssetKey(buf)
77  }
78}
79
80/// A Content-Security-Policy hash value for a specific directive.
81/// For more information see [the MDN page](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives).
82#[non_exhaustive]
83#[derive(Debug, Clone, Copy)]
84pub enum CspHash<'a> {
85  /// The `script-src` directive.
86  Script(&'a str),
87
88  /// The `style-src` directive.
89  Style(&'a str),
90}
91
92impl CspHash<'_> {
93  /// The Content-Security-Policy directive this hash applies to.
94  pub fn directive(&self) -> &'static str {
95    match self {
96      Self::Script(_) => "script-src",
97      Self::Style(_) => "style-src",
98    }
99  }
100
101  /// The value of the Content-Security-Policy hash.
102  pub fn hash(&self) -> &str {
103    match self {
104      Self::Script(hash) => hash,
105      Self::Style(hash) => hash,
106    }
107  }
108}
109
110/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
111#[derive(Debug)]
112pub struct EmbeddedAssets {
113  assets: phf::Map<&'static str, &'static [u8]>,
114  // Hashes that must be injected to the CSP of every HTML file.
115  global_hashes: &'static [CspHash<'static>],
116  // Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
117  html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
118}
119
120impl EmbeddedAssets {
121  /// Creates a new instance from the given asset map and script hash list.
122  pub const fn new(
123    map: phf::Map<&'static str, &'static [u8]>,
124    global_hashes: &'static [CspHash<'static>],
125    html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
126  ) -> Self {
127    Self {
128      assets: map,
129      global_hashes,
130      html_hashes,
131    }
132  }
133
134  /// Get an asset by key.
135  #[cfg(feature = "compression")]
136  pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
137    self
138      .assets
139      .get(key.as_ref())
140      .map(|&(mut asdf)| {
141        // with the exception of extremely small files, output should usually be
142        // at least as large as the compressed version.
143        let mut buf = Vec::with_capacity(asdf.len());
144        brotli::BrotliDecompress(&mut asdf, &mut buf).map(|()| buf)
145      })
146      .and_then(Result::ok)
147      .map(Cow::Owned)
148  }
149
150  /// Get an asset by key.
151  #[cfg(not(feature = "compression"))]
152  pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
153    self
154      .assets
155      .get(key.as_ref())
156      .copied()
157      .map(|a| Cow::Owned(a.to_vec()))
158  }
159
160  /// Iterate on the assets.
161  pub fn iter(&self) -> Box<AssetsIter<'_>> {
162    Box::new(
163      self
164        .assets
165        .into_iter()
166        .map(|(k, b)| (Cow::Borrowed(*k), Cow::Borrowed(*b))),
167    )
168  }
169
170  /// CSP hashes for the given asset.
171  pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
172    Box::new(
173      self
174        .global_hashes
175        .iter()
176        .chain(
177          self
178            .html_hashes
179            .get(html_path.as_ref())
180            .copied()
181            .into_iter()
182            .flatten(),
183        )
184        .copied(),
185    )
186  }
187}