Move asset hashing into the CLI (#3988)

* Fix deduping assets

* hash assets in the linker

* fix extracting assets

* Fix the hashed asset name

* deserialize the asset back from the link section

* prevent the read from being inlined

* remove some extra logs

* Fix liveview routing

* Only look for direct dependencies on dioxus features

* fix wasm split test

* On wasm, create a new object file with the asset information

* Fix multiple assets

* add a regression test for 3467

* fix image link

* fix linker out file

* fix linux linker args

* add windows linker command

* fix linux link args

* fix windows target triple

* Expose an option to build assets in the linker intercept

* Fix the standalone optimizer and document it in the dx help command

* include the cli opt version in the hash

* fix clippy

* fix ssg port

* add a safety note to the read_volatile call

* move found assets log

* fix some merge conflicts

* fix build

* make most env var for the linker optional

* guess the target triple when dx acts as the linker intercept

* automatically find the linker for normal builds

* use the right linker in wasm mode

* fix mac linker

* look inside archive files (rlibs)

* remove out.txt

* fix clippy

* fix typo

* fix clippy

* fix build

* fix dx link name in docs

* ignore libcompiler_builtins warnings

* fix playwright

* pass typocheck

* fix printing sysroot

* Modify final binary

* fix wasm assets

* remove old link section logic

* fix tests

* forward manganis export args

* fail to evaluate relocatable address

* retain all exports in the same pattern as other platforms

* Fix adding assets during a wasm hot patch

* less nesting and remove logs

* fix clippy

* log pdb files

* read from pdb

* print data section contents

* print with object

* normal windows builds working

* fix hot patch windows

* clean up logs

* fix clippy

* clean up assets

* use the most recent pdb

* fix doc test

* fix typo

* tiny nits

* make bundled a reference instead of a pointer

* add helpful comment to impl

* match on file format

* Fix asset hashing comment

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-05-31 21:45:12 +02:00
1 parent f865e3f559
commit 58669f892d
45 files changed
+1174 -762

No files matched your search

+75 -33
View File
@@ -1,23 +1,13 @@
use crate::AssetOptions;
use const_serialize::{ConstStr, SerializeConst};
use std::path::PathBuf;
use const_serialize::{deserialize_const, ConstStr, ConstVec, SerializeConst};
use std::{fmt::Debug, hash::Hash, path::PathBuf};
/// An asset that should be copied by the bundler with some options. This type will be
/// serialized into the binary and added to the link section [`LinkSection::CURRENT`](crate::linker::LinkSection::CURRENT).
/// serialized into the binary.
/// CLIs that support manganis, should pull out the assets from the link section, optimize,
/// and write them to the filesystem at [`BundledAsset::bundled_path`] for the application
/// to use.
#[derive(
Debug,
PartialEq,
PartialOrd,
Clone,
Copy,
Hash,
SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[derive(Debug, Eq, Clone, Copy, SerializeConst, serde::Serialize, serde::Deserialize)]
pub struct BundledAsset {
/// The absolute path of the asset
absolute_source_path: ConstStr,
@@ -27,13 +17,46 @@ pub struct BundledAsset {
options: AssetOptions,
}
impl PartialEq for BundledAsset {
fn eq(&self, other: &Self) -> bool {
self.absolute_source_path == other.absolute_source_path
&& self.bundled_path == other.bundled_path
&& self.options == other.options
}
}
impl PartialOrd for BundledAsset {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self
.absolute_source_path
.partial_cmp(&other.absolute_source_path)
{
Some(core::cmp::Ordering::Equal) => {}
ord => return ord,
}
match self.bundled_path.partial_cmp(&other.bundled_path) {
Some(core::cmp::Ordering::Equal) => {}
ord => return ord,
}
self.options.partial_cmp(&other.options)
}
}
impl Hash for BundledAsset {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.absolute_source_path.hash(state);
self.bundled_path.hash(state);
self.options.hash(state);
}
}
impl BundledAsset {
#[doc(hidden)]
/// This should only be called from the macro
/// Create a new asset
pub const fn new(
absolute_source_path: &'static str,
bundled_path: &'static str,
absolute_source_path: &str,
bundled_path: &str,
options: AssetOptions,
) -> Self {
Self {
@@ -83,6 +106,7 @@ impl BundledAsset {
pub fn absolute_source_path(&self) -> &str {
self.absolute_source_path.as_str()
}
/// Get the options for the asset
pub const fn options(&self) -> &AssetOptions {
&self.options
@@ -101,28 +125,49 @@ impl BundledAsset {
/// img { src: ASSET }
/// };
/// ```
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(PartialEq, Clone, Copy)]
pub struct Asset {
/// The bundled asset
bundled: BundledAsset,
/// The link section for the asset
keep_link_section: fn() -> u8,
/// A pointer to the bundled asset. This will be resolved after the linker has run and
/// put into the lazy asset
///
/// WARNING: Don't read this directly. Reads can get optimized away at compile time before
/// the data for this is filled in by the CLI after the binary is built. Instead, use
/// [`std::ptr::read_volatile`] to read the data.
bundled: &'static [u8],
}
impl Debug for Asset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.resolve().fmt(f)
}
}
unsafe impl Send for Asset {}
unsafe impl Sync for Asset {}
impl Asset {
#[doc(hidden)]
/// This should only be called from the macro
/// Create a new asset from the bundled form of the asset and the link section
pub const fn new(bundled: BundledAsset, keep_link_section: fn() -> u8) -> Self {
Self {
bundled,
keep_link_section,
}
pub const fn new(bundled: &'static [u8]) -> Self {
Self { bundled }
}
/// Get the bundled asset
pub const fn bundled(&self) -> &BundledAsset {
&self.bundled
pub fn bundled(&self) -> BundledAsset {
let len = self.bundled.len();
let ptr = self.bundled as *const [u8] as *const u8;
if ptr.is_null() {
panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
}
let mut bytes = ConstVec::new();
for byte in 0..len {
// SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
// since we are reading a u8 there are no alignment requirements
bytes = bytes.push(unsafe { std::ptr::read_volatile(ptr.add(byte)) });
}
let read = bytes.read();
deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1
}
/// Return a canonicalized path to the asset
@@ -130,13 +175,10 @@ impl Asset {
/// Attempts to resolve it against an `assets` folder in the current directory.
/// If that doesn't exist, it will resolve against the cargo manifest dir
pub fn resolve(&self) -> PathBuf {
// Force a volatile read of the asset link section to ensure the symbol makes it into the binary
(self.keep_link_section)();
#[cfg(feature = "dioxus")]
// If the asset is relative, we resolve the asset at the current directory
if !dioxus_core_types::is_bundled_app() {
return PathBuf::from(self.bundled.absolute_source_path.as_str());
return PathBuf::from(self.bundled().absolute_source_path.as_str());
}
#[cfg(feature = "dioxus")]
@@ -156,7 +198,7 @@ impl Asset {
// Otherwise presumably we're bundled and we can use the bundled path
bundle_root.join(PathBuf::from(
self.bundled.bundled_path.as_str().trim_start_matches('/'),
self.bundled().bundled_path.as_str().trim_start_matches('/'),
))
}
}
@@ -5,6 +5,7 @@ use std::collections::HashSet;
/// Options for a css asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -79,6 +80,7 @@ impl CssAssetOptions {
/// Options for a css module asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -5,6 +5,7 @@ use crate::AssetOptions;
/// The builder for a folder asset.
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -1,94 +0,0 @@
//! Utilities for creating hashed paths to assets in Manganis. This module defines [`AssetHash`] which is used to create a hashed path to an asset in both the CLI and the macro.
use std::{
error::Error,
hash::{Hash, Hasher},
io::Read,
path::{Path, PathBuf},
};
/// An error that can occur when hashing an asset
#[derive(Debug)]
#[non_exhaustive]
pub enum AssetHashError {
/// An io error occurred
IoError { err: std::io::Error, path: PathBuf },
}
impl std::fmt::Display for AssetHashError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetHashError::IoError { path, err } => {
write!(f, "Failed to read file: {}; {}", path.display(), err)
}
}
}
}
impl Error for AssetHashError {}
/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
/// change. This hash is included in the URL of the bundled asset for cache busting.
pub struct AssetHash {
/// We use a wrapper type here to hide the exact size of the hash so we can switch to a sha hash in a minor version bump
hash: [u8; 8],
}
impl AssetHash {
/// Create a new asset hash
const fn new(hash: u64) -> Self {
Self {
hash: hash.to_le_bytes(),
}
}
/// Get the hash bytes
pub const fn bytes(&self) -> &[u8] {
&self.hash
}
/// Create a new asset hash for a file. The input file to this function should be fully resolved
pub fn hash_file_contents(file_path: &Path) -> Result<AssetHash, AssetHashError> {
// Create a hasher
let mut hash = std::collections::hash_map::DefaultHasher::new();
// If this is a folder, hash the folder contents
if file_path.is_dir() {
let files = std::fs::read_dir(file_path).map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
for file in files.flatten() {
let path = file.path();
Self::hash_file_contents(&path)?.bytes().hash(&mut hash);
}
let hash = hash.finish();
return Ok(AssetHash::new(hash));
}
// Otherwise, open the file to get its contents
let mut file = std::fs::File::open(file_path).map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
// know the file contents, so we only include that hash
let mut buffer = [0; 8192];
loop {
let read = file
.read(&mut buffer)
.map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
if read == 0 {
break;
}
hash.write(&buffer[..read]);
}
Ok(AssetHash::new(hash.finish()))
}
}
@@ -5,6 +5,7 @@ use crate::AssetOptions;
/// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types)
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -31,6 +32,7 @@ pub enum ImageFormat {
/// The size of an image asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -56,6 +58,7 @@ pub enum ImageSize {
/// Options for an image asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -5,6 +5,7 @@ use crate::AssetOptions;
/// Options for a javascript asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
@@ -15,7 +15,3 @@ pub use js::*;
mod asset;
pub use asset::*;
pub mod linker;
pub mod hash;
@@ -1,71 +0,0 @@
//! Utilities for working with Manganis assets in the linker. This module defines [`LinkSection`] which has information about what section manganis assets are stored in on each platform.
/// Information about the manganis link section for a given platform
#[derive(Debug, Clone, Copy)]
pub struct LinkSection {
/// The link section we pass to the static
pub link_section: &'static str,
/// The name of the section we find in the binary
pub name: &'static str,
}
impl LinkSection {
/// The list of link sections for all supported platforms
pub const ALL: &'static [&'static LinkSection] =
&[Self::WASM, Self::MACOS, Self::WINDOWS, Self::ILLUMOS];
/// Returns the link section used in linux, android, fuchsia, psp, freebsd, and wasm32
pub const WASM: &'static LinkSection = &LinkSection {
link_section: "manganis",
name: "manganis",
};
/// Returns the link section used in macOS, iOS, tvOS
pub const MACOS: &'static LinkSection = &LinkSection {
link_section: "__DATA,manganis,regular,no_dead_strip",
name: "manganis",
};
/// Returns the link section used in windows
pub const WINDOWS: &'static LinkSection = &LinkSection {
link_section: "mg",
name: "mg",
};
/// Returns the link section used in illumos
pub const ILLUMOS: &'static LinkSection = &LinkSection {
link_section: "set_manganis",
name: "set_manganis",
};
/// The link section used on the current platform
pub const CURRENT: &'static LinkSection = {
#[cfg(any(
target_os = "none",
target_os = "linux",
target_os = "android",
target_os = "fuchsia",
target_os = "psp",
target_os = "freebsd",
target_arch = "wasm32"
))]
{
Self::WASM
}
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))]
{
Self::MACOS
}
#[cfg(target_os = "windows")]
{
Self::WINDOWS
}
#[cfg(target_os = "illumos")]
{
Self::ILLUMOS
}
};
}
@@ -7,6 +7,7 @@ use crate::{
/// Settings for a generic asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
+20 -19
View File
@@ -1,9 +1,11 @@
use crate::{resolve_path, AssetParseError};
use macro_string::MacroString;
use manganis_core::hash::AssetHash;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use std::path::PathBuf;
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned as _,
@@ -72,24 +74,19 @@ impl ToTokens for AssetParser {
return;
}
};
let asset_str = asset.to_string_lossy();
let mut asset_str = proc_macro2::Literal::string(&asset_str);
let asset_string = asset.to_string_lossy();
let mut asset_str = proc_macro2::Literal::string(&asset_string);
asset_str.set_span(self.path_expr.span());
let hash = match AssetHash::hash_file_contents(asset) {
Ok(hash) => hash,
Err(err) => {
let err = err.to_string();
tokens.append_all(quote! { compile_error!(#err) });
return;
}
};
let hash = hash.bytes();
let mut hash = DefaultHasher::new();
format!("{:?}", self.options.span()).hash(&mut hash);
format!("{:?}", self.options.to_string()).hash(&mut hash);
asset_string.hash(&mut hash);
let asset_hash = format!("{:016x}", hash.finish());
// Generate the link section for the asset
// The link section includes the source path and the output path of the asset
let link_section = crate::generate_link_section(quote!(__ASSET));
let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash);
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
let constructor = if asset.is_relative() {
@@ -106,21 +103,25 @@ impl ToTokens for AssetParser {
tokens.extend(quote! {
{
// We keep a hash of the contents of the asset for cache busting
const __ASSET_HASH: &[u8] = &[#(#hash),*];
// The source is used by the CLI to copy the asset
const __ASSET_SOURCE_PATH: &'static str = #asset_str;
// The options give the CLI info about how to process the asset
// Note: into_asset_options is not a trait, so we cannot accept the options directly
// in the constructor. Stable rust doesn't have support for constant functions in traits
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
// The input token hash is used to uniquely identify the link section for this asset
const __ASSET_HASH: &'static str = #asset_hash;
// Create the asset that the crate will use. This is used both in the return value and
// added to the linker for the bundler to copy later
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_HASH, __ASSET_OPTIONS);
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_OPTIONS);
#link_section
manganis::Asset::new(__ASSET, __keep_link_section)
static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION;
manganis::Asset::new(
__REFERENCE_TO_LINK_SECTION
)
}
})
}
+4 -12
View File
@@ -6,12 +6,9 @@ use quote::ToTokens;
/// We force rust to store a serialized representation of the asset description
/// inside a particular region of the binary, with the label "manganis".
/// After linking, the "manganis" sections of the different object files will be merged.
pub fn generate_link_section(asset: impl ToTokens) -> TokenStream2 {
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
let position = proc_macro2::Span::call_site();
let section_name = syn::LitStr::new(
manganis_core::linker::LinkSection::CURRENT.link_section,
position,
);
let export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
quote::quote! {
// First serialize the asset into a constant sized buffer
@@ -22,12 +19,7 @@ pub fn generate_link_section(asset: impl ToTokens) -> TokenStream2 {
const __LEN: usize = __BYTES.len();
// Now that we have the size of the asset, copy the bytes into a static array
#[link_section = #section_name]
#[used]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
fn __keep_link_section() -> u8 {
unsafe { std::ptr::read_volatile(__LINK_SECTION.as_ptr()) }
}
#[unsafe(export_name = #export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
}
}
-39
View File
@@ -1,39 +0,0 @@
use const_serialize::{serialize_const, ConstVec, SerializeConst};
// From rustchash - https://github.com/rust-lang/rustc-hash/blob/6745258da00b7251bed4a8461871522d0231a9c7/src/lib.rs#L98
const K: u64 = 0xf1357aea2e62a9c5;
pub(crate) struct ConstHasher {
hash: u64,
}
impl ConstHasher {
pub const fn new() -> Self {
Self { hash: 0 }
}
pub const fn finish(&self) -> u64 {
self.hash
}
pub const fn write(mut self, bytes: &[u8]) -> Self {
let mut i = 0;
while i < bytes.len() {
self = self.write_byte(bytes[i]);
i += 1;
}
self
}
pub const fn write_byte(mut self, byte: u8) -> Self {
self.hash = self.hash.wrapping_add(byte as u64).wrapping_mul(K);
self
}
pub const fn hash_by_bytes<T: SerializeConst>(self, item: &T) -> Self {
let mut bytes = ConstVec::new();
bytes = serialize_const(item, bytes);
let bytes = bytes.as_ref();
self.write(bytes)
}
}
-1
View File
@@ -1,7 +1,6 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
mod hash;
#[doc(hidden)]
pub mod macro_helpers;
pub use manganis_macro::{asset, css_module};
+22 -201
View File
@@ -1,17 +1,13 @@
pub use const_serialize;
use const_serialize::{serialize_const, ConstStr, ConstVec};
use const_serialize::{serialize_const, ConstStr, ConstVec, SerializeConst};
use manganis_core::{AssetOptions, BundledAsset};
use crate::hash::ConstHasher;
const PLACEHOLDER_HASH: ConstStr =
ConstStr::new("this is a placeholder path which will be replaced by the linker");
/// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(
input_path: &str,
content_hash: &[u8],
asset_config: AssetOptions,
) -> BundledAsset {
let hashed_path = generate_unique_path_with_byte_hash(input_path, content_hash, &asset_config);
BundledAsset::new_from_const(ConstStr::new(input_path), hashed_path, asset_config)
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
BundledAsset::new_from_const(ConstStr::new(input_path), PLACEHOLDER_HASH, asset_config)
}
/// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
@@ -22,204 +18,29 @@ pub const fn create_bundled_asset(
)]
pub const fn create_bundled_asset_relative(
input_path: &str,
content_hash: &[u8],
asset_config: AssetOptions,
) -> BundledAsset {
create_bundled_asset(input_path, content_hash, asset_config)
}
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
pub const fn generate_unique_path(
input_path: &str,
content_hash: u64,
asset_config: &AssetOptions,
) -> ConstStr {
let byte_hash = content_hash.to_le_bytes();
generate_unique_path_with_byte_hash(input_path, &byte_hash, asset_config)
}
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
const fn generate_unique_path_with_byte_hash(
input_path: &str,
content_hash: &[u8],
asset_config: &AssetOptions,
) -> ConstStr {
// Format the unique path with the format `{input_path}-{hash}.{extension}`
// Start with the input path
let mut input_path = ConstStr::new(input_path);
// Then strip the prefix from the input path. The path comes from the build platform, but
// in wasm, we don't know what the path separator is from the build platform. We need to
// split by both unix and windows paths and take the smallest one
let mut extension = None;
match (input_path.rsplit_once('/'), input_path.rsplit_once('\\')) {
(Some((_, unix_new_input_path)), Some((_, windows_new_input_path))) => {
input_path = if unix_new_input_path.len() < windows_new_input_path.len() {
unix_new_input_path
} else {
windows_new_input_path
};
}
(Some((_, unix_new_input_path)), _) => {
input_path = unix_new_input_path;
}
(_, Some((_, windows_new_input_path))) => {
input_path = windows_new_input_path;
}
_ => {}
}
if let Some((new_input_path, new_extension)) = input_path.rsplit_once('.') {
extension = Some(new_extension);
input_path = new_input_path;
}
// Then add a dash
let mut macro_output_path = input_path.push_str("-");
// Hash the contents along with the asset config to create a unique hash for the asset
// When this hash changes, the client needs to re-fetch the asset
let mut hasher = ConstHasher::new();
hasher = hasher.write(content_hash);
hasher = hasher.hash_by_bytes(asset_config);
let hash = hasher.finish();
// Then add the hash in hex form
let hash_bytes = hash.to_le_bytes();
let mut i = 0;
while i < hash_bytes.len() {
let byte = hash_bytes[i];
let first = byte >> 4;
let second = byte & 0x0f;
const fn byte_to_char(byte: u8) -> char {
match char::from_digit(byte as u32, 16) {
Some(c) => c,
None => panic!("byte must be a valid digit"),
}
}
macro_output_path = macro_output_path.push(byte_to_char(first));
macro_output_path = macro_output_path.push(byte_to_char(second));
i += 1;
}
// Finally add the extension
match asset_config.extension() {
Some(extension) => {
macro_output_path = macro_output_path.push('.');
macro_output_path = macro_output_path.push_str(extension)
}
None => {
if let Some(extension) = extension {
macro_output_path = macro_output_path.push('.');
let ext_bytes = extension.as_str().as_bytes();
// Rewrite scss as css
if bytes_equal(ext_bytes, b"scss") || bytes_equal(ext_bytes, b"sass") {
macro_output_path = macro_output_path.push_str("css")
} else {
macro_output_path = macro_output_path.push_str(extension.as_str())
}
}
}
}
macro_output_path
}
/// Construct the hash used by manganis and cli-opt to uniquely identify a asset based on its contents
pub const fn hash_asset(asset_config: &AssetOptions, content_hash: u64) -> ConstStr {
let mut string = ConstStr::new("");
// Hash the contents along with the asset config to create a unique hash for the asset
// When this hash changes, the client needs to re-fetch the asset
let mut hasher = ConstHasher::new();
hasher = hasher.write(&content_hash.to_le_bytes());
hasher = hasher.hash_by_bytes(asset_config);
let hash = hasher.finish();
// Then add the hash in hex form
let hash_bytes = hash.to_le_bytes();
let mut i = 0;
while i < hash_bytes.len() {
let byte = hash_bytes[i];
let first = byte >> 4;
let second = byte & 0x0f;
const fn byte_to_char(byte: u8) -> char {
match char::from_digit(byte as u32, 16) {
Some(c) => c,
None => panic!("byte must be a valid digit"),
}
}
string = string.push(byte_to_char(first));
string = string.push(byte_to_char(second));
i += 1;
}
string
}
const fn bytes_equal(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut i = 0;
while i < left.len() {
if left[i] != right[i] {
return false;
}
i += 1;
}
true
}
#[test]
fn test_unique_path() {
use manganis_core::{ImageAssetOptions, ImageFormat};
use std::path::PathBuf;
let mut input_path = PathBuf::from("some");
input_path.push("prefix");
input_path.push("test.png");
let content_hash = 123456789;
let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
let output_path =
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
// Changing the path without changing the contents shouldn't change the hash
let mut input_path = PathBuf::from("some");
input_path.push("prefix");
input_path.push("prefix");
input_path.push("test.png");
let content_hash = 123456789;
let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
let output_path =
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
let mut input_path = PathBuf::from("test");
input_path.push("ing");
input_path.push("test");
let content_hash = 123456789;
let asset_config = AssetOptions::Unknown;
let output_path =
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
assert_eq!(output_path.as_str(), "test-8d6e32dc0b45f853");
// Just changing the content hash should change the total hash
let mut input_path = PathBuf::from("test");
input_path.push("ing");
input_path.push("test");
let content_hash = 123456780;
let asset_config = AssetOptions::Unknown;
let output_path =
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
assert_eq!(output_path.as_str(), "test-40783366737abc4d");
create_bundled_asset(input_path, asset_config)
}
/// Serialize an asset to a const buffer
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
let write = ConstVec::new();
serialize_const(asset, write)
let data = ConstVec::new();
let mut data = serialize_const(asset, data);
// Reserve the maximum size of the asset
while data.len() < BundledAsset::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
}
/// Deserialize a const buffer into a BundledAsset
pub const fn deserialize_asset(bytes: &[u8]) -> BundledAsset {
let bytes = ConstVec::new().extend(bytes);
match const_serialize::deserialize_const!(BundledAsset, bytes.read()) {
Some((_, asset)) => asset,
None => panic!("Failed to deserialize asset. This may be caused by a mismatch between your dioxus and dioxus-cli versions"),
}
}
/// Copy a slice into a constant sized buffer at compile time