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