Skip to main content

manganis_core/
asset.rs

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