Skip to main content

manganis_core/
asset.rs

1use crate::AssetOptions;
2use const_serialize_07 as const_serialize;
3use const_serialize_08::{deserialize_const, ConstStr, SerializeConst};
4use std::{fmt::Debug, hash::Hash, path::PathBuf};
5
6/// An asset that should be copied by the bundler with some options. This type will be
7/// serialized into the binary.
8/// CLIs that support manganis, should pull out the assets from the link section, optimize,
9/// and write them to the filesystem at [`BundledAsset::bundled_path`] for the application
10/// to use.
11#[derive(
12    Debug,
13    Eq,
14    Clone,
15    Copy,
16    SerializeConst,
17    const_serialize::SerializeConst,
18    serde::Serialize,
19    serde::Deserialize,
20)]
21#[const_serialize(crate = const_serialize_08)]
22pub struct BundledAsset {
23    /// The absolute path of the asset
24    absolute_source_path: ConstStr,
25
26    /// The bundled path of the asset
27    bundled_path: ConstStr,
28
29    /// The options for the asset
30    options: AssetOptions,
31}
32
33impl PartialEq for BundledAsset {
34    fn eq(&self, other: &Self) -> bool {
35        self.absolute_source_path == other.absolute_source_path
36            && self.bundled_path == other.bundled_path
37            && self.options == other.options
38    }
39}
40
41impl PartialOrd for BundledAsset {
42    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
43        match self
44            .absolute_source_path
45            .partial_cmp(&other.absolute_source_path)
46        {
47            Some(core::cmp::Ordering::Equal) => {}
48            ord => return ord,
49        }
50        match self.bundled_path.partial_cmp(&other.bundled_path) {
51            Some(core::cmp::Ordering::Equal) => {}
52            ord => return ord,
53        }
54        self.options.partial_cmp(&other.options)
55    }
56}
57
58impl Hash for BundledAsset {
59    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
60        self.absolute_source_path.hash(state);
61        self.bundled_path.hash(state);
62        self.options.hash(state);
63    }
64}
65
66impl BundledAsset {
67    pub const PLACEHOLDER_HASH: &str = "This should be replaced by dx as part of the build process. If you see this error, make sure you are using a matching version of dx and dioxus and you are not stripping symbols from your binary.";
68
69    #[doc(hidden)]
70    /// This should only be called from the macro
71    /// Create a new asset
72    pub const fn new(
73        absolute_source_path: &str,
74        bundled_path: &str,
75        options: AssetOptions,
76    ) -> Self {
77        Self {
78            absolute_source_path: ConstStr::new(absolute_source_path),
79            bundled_path: ConstStr::new(bundled_path),
80            options,
81        }
82    }
83
84    /// Get the bundled name of the asset. This identifier cannot be used to read the asset directly
85    pub fn bundled_path(&self) -> &str {
86        self.bundled_path.as_str()
87    }
88
89    /// Get the absolute path of the asset source. This path will not be available when the asset is bundled
90    pub fn absolute_source_path(&self) -> &str {
91        self.absolute_source_path.as_str()
92    }
93
94    /// Get the options for the asset
95    pub const fn options(&self) -> &AssetOptions {
96        &self.options
97    }
98}
99
100/// A bundled asset with some options. The asset can be used in rsx! to reference the asset.
101/// It should not be read directly with [`std::fs::read`] because the path needs to be resolved
102/// relative to the bundle
103///
104/// ```rust, ignore
105/// # use manganis::{asset, Asset};
106/// # use dioxus::prelude::*;
107/// const ASSET: Asset = asset!("/assets/image.png");
108/// rsx! {
109///     img { src: ASSET }
110/// };
111/// ```
112#[allow(unpredictable_function_pointer_comparisons)]
113#[derive(PartialEq, Clone, Copy)]
114pub struct Asset {
115    /// A function that returns a pointer to the bundled asset. This will be resolved after the linker has run and
116    /// put into the lazy asset. We use a function instead of using the pointer directly to force the compiler to
117    /// read the static __LINK_SECTION at runtime which will be offset by the hot reloading engine instead
118    /// of at compile time which can't be offset
119    ///
120    /// WARNING: Don't read this directly. Reads can get optimized away at compile time before
121    /// the data for this is filled in by the CLI after the binary is built. Instead, use
122    /// [`std::ptr::read_volatile`] to read the data.
123    bundled: fn() -> &'static [u8],
124    /// The legacy version of [`Self::bundled`]. This is only used for backwards compatibility with older versions of the CLI
125    legacy: fn() -> &'static [u8],
126}
127
128impl Debug for Asset {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        self.resolve().fmt(f)
131    }
132}
133
134unsafe impl Send for Asset {}
135unsafe impl Sync for Asset {}
136
137impl Asset {
138    #[doc(hidden)]
139    /// This should only be called from the macro
140    /// Create a new asset from the bundled form of the asset and the link section
141    pub const fn new(
142        bundled: extern "Rust" fn() -> &'static [u8],
143        legacy: extern "Rust" fn() -> &'static [u8],
144    ) -> Self {
145        Self { bundled, legacy }
146    }
147
148    /// Get the bundled asset
149    pub fn bundled(&self) -> BundledAsset {
150        // Read the slice using volatile reads to prevent the compiler from optimizing
151        // away the read at compile time
152        fn read_slice_volatile(bundled: &'static [u8]) -> Vec<u8> {
153            let ptr = bundled as *const [u8] as *const u8;
154            let len = bundled.len();
155            if ptr.is_null() {
156                panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
157            }
158            let mut bytes = Vec::with_capacity(len);
159            for byte in 0..len {
160                // SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
161                // since we are reading a u8 there are no alignment requirements
162                let byte = unsafe { std::ptr::read_volatile(ptr.add(byte)) };
163                bytes.push(byte);
164            }
165            bytes
166        }
167
168        let bundled = (self.bundled)();
169        let bytes = read_slice_volatile(bundled);
170        let read = bytes.as_slice();
171        let asset = deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
172
173        // If the asset wasn't bundled with the newer format, try the legacy format
174        if asset.bundled_path() == BundledAsset::PLACEHOLDER_HASH {
175            let bundled = (self.legacy)();
176            let bytes = read_slice_volatile(bundled);
177            let read = const_serialize_07::ConstReadBuffer::new(bytes.as_ref());
178            let asset = const_serialize_07::deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
179            asset
180        } else {
181            asset
182        }
183    }
184
185    /// Return a canonicalized path to the asset
186    ///
187    /// Attempts to resolve it against an `assets` folder in the current directory.
188    /// If that doesn't exist, it will resolve against the cargo manifest dir
189    pub fn resolve(&self) -> PathBuf {
190        #[cfg(feature = "dioxus")]
191        // If the asset is relative, we resolve the asset at the current directory
192        if !dioxus_core_types::is_bundled_app() {
193            return PathBuf::from(self.bundled().absolute_source_path.as_str());
194        }
195
196        #[cfg(feature = "dioxus")]
197        let bundle_root = {
198            let base_path = dioxus_cli_config::base_path();
199            let base_path = base_path
200                .as_deref()
201                .map(|base_path| {
202                    let trimmed = base_path.trim_matches('/');
203                    format!("/{trimmed}")
204                })
205                .unwrap_or_default();
206            PathBuf::from(format!("{base_path}/assets/"))
207        };
208        #[cfg(not(feature = "dioxus"))]
209        let bundle_root = PathBuf::from("/assets/");
210
211        // Otherwise presumably we're bundled and we can use the bundled path
212        bundle_root.join(PathBuf::from(
213            self.bundled().bundled_path.as_str().trim_start_matches('/'),
214        ))
215    }
216}
217
218impl From<Asset> for String {
219    fn from(value: Asset) -> Self {
220        value.to_string()
221    }
222}
223impl From<Asset> for Option<String> {
224    fn from(value: Asset) -> Self {
225        Some(value.to_string())
226    }
227}
228
229impl std::fmt::Display for Asset {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        write!(f, "{}", self.resolve().display())
232    }
233}
234
235#[cfg(feature = "dioxus")]
236impl dioxus_core_types::DioxusFormattable for Asset {
237    fn format(&self) -> std::borrow::Cow<'static, str> {
238        std::borrow::Cow::Owned(self.to_string())
239    }
240}