Switch const serialize to use CBOR (#4932)

* implement const cbor encoding + decoding for basic types

* implement map encoding and decoding

* fix map test

* all tests passing

* dynamically sized arrays

* smaller str encoding

* swap array and list and add more documentation

* move serialization and deserialization for each struct together

* support both legacy and new assets

* bump const serialize version

* fix asset conversion code

* pull out manganis symbol struct from tuble

* test reading old asset versions

* more list/array cleanup and bump rust version

* fix clippy

* switch cli opt port

* install playwright deps

* split cli-optimization package to avoid two dioxus' in the same crate

* fix clippy

* force the toast.png asset to be included in the linker stage #[used] has platform depedant behavior

* bundle both formats in the link section and only use the asset the cli changed

* remove sudo which doesn't exist on windows

* add binstall to the playwright workflow

* separate bin target for the old cli

* lockfile

* cargo update to get versions shaken out

* notes on binstalls, add back `.read()` for compat

* use 0.8.0-alpha.0 for const-serialize instead of 0.8

this ensures we can still publish breaking changes on 0.8 without taking over the full version

* fix clippy in example

* small debug nits

* update rust-overlay in nix

* fix flake?

* migrate to new structure

* just comment out binstalls

* just remove old test harness

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-12-28 21:49:34 -05:00
1 parent c68a47b2d3
commit c977cc125e
48 files changed
+3328 -1949

No files matched your search

Generated
+973 -863
View File
File diff suppressed because it is too large. Load diff
+11 -3
View File
@@ -194,9 +194,14 @@ dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.2" }
dioxus-cli-telemetry = { path = "packages/cli-telemetry", version = "0.7.2" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.7.2" }
# const-serializea
const-serialize = { path = "packages/const-serialize", version = "0.7.2" }
const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.7.2" }
# const-serialize. these are on "alpha" versions, but really, we are prepping for 0.8 but not committing to it yet
# once the workspace moves onto 0.8, we can clean this up, moving the const-serialize stuff back to normal versions
const-serialize = { path = "packages/const-serialize", version = "0.8.0-alpha.0" }
const-serialize-08 = { package = "const-serialize", path = "packages/const-serialize", version = "0.8.0-alpha.0" }
const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.8.0-alpha.0" }
# The version of const-serialize published with 0.7.0 and 0.7.1 that the CLI should still support
const-serialize-07 = { package = "const-serialize", version = "=0.7.2" }
# subsecond
subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.7.2" }
@@ -207,6 +212,9 @@ manganis = { path = "packages/manganis/manganis", version = "0.7.2" }
manganis-core = { path = "packages/manganis/manganis-core", version = "0.7.2" }
manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.7.2" }
# The version of assets published with 0.7.0 and 0.7.1 that the CLI should still support
manganis-core-07 = { package = "manganis-core", version = "=0.7.2" }
# wasm-split
wasm-splitter = { path = "packages/wasm-split/wasm-split", version = "0.7.2" }
wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.7.2" }
+1 -1
View File
@@ -48,7 +48,7 @@ fn app() -> Element {
//
// To add multiple middleware layers, simply stack multiple `#[middleware]` attributes.
#[get("/api/count")]
#[middleware(TimeoutLayer::new(Duration::from_secs(3)))]
#[middleware(TimeoutLayer::with_status_code(408.try_into().unwrap(), Duration::from_secs(3)))]
async fn per_route_middleware() -> Result<String> {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok("Hello, world!".to_string())
Generated
+12 -12
View File
@@ -5,11 +5,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"lastModified": 1765835352,
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"lastModified": 1766870016,
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
"type": "github"
},
"original": {
@@ -36,11 +36,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"lastModified": 1765674936,
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
"type": "github"
},
"original": {
@@ -78,11 +78,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1744684506,
"narHash": "sha256-pDPDMT1rdkTWi8MIoZ67gT3L817R7P0Jo+PP+BrnyJI=",
"lastModified": 1766890375,
"narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "47beae969336c05e892e1e4a9dbaac9593de34ab",
"rev": "91e1f7a0017065360f447622d11b7ce6ed04772f",
"type": "github"
},
"original": {
+4 -10
View File
@@ -43,16 +43,10 @@
pkgs.webkitgtk_4_1
pkgs.xdotool
]
++ lib.optionals pkgs.stdenv.isDarwin (
with pkgs.darwin.apple_sdk.frameworks;
[
IOKit
Carbon
WebKit
Security
Cocoa
]
);
++ lib.optionals pkgs.stdenv.isDarwin [
pkgs.apple-sdk
pkgs.libiconv
];
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
+2
View File
@@ -98,6 +98,7 @@ brotli = "8.0.1"
ignore = "0.4.23"
env_logger = { workspace = true }
const-serialize = { workspace = true, features = ["serde"] }
const-serialize-07 = { workspace = true, features = ["serde"] }
tracing-subscriber = { version = "0.3.19", features = [
"std",
@@ -122,6 +123,7 @@ log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] }
tempfile = "3.19.1"
manganis = { workspace = true }
manganis-core = { workspace = true }
manganis-core-07 = { workspace = true }
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
wasm-encoder = "0.235.0"
+229 -41
View File
@@ -11,10 +11,10 @@
//! process in the build system.
//!
//! We use the same lessons learned from the hot-patching engine which parses the binary file and its
//! symbol table to find symbols that match the `__MANGANIS__` prefix. These symbols are ideally data
//! symbol table to find symbols that match the `__ASSETS__` prefix. These symbols are ideally data
//! symbols and contain the BundledAsset data type which implements ConstSerialize and ConstDeserialize.
//!
//! When the binary is built, the `dioxus asset!()` macro will emit its metadata into the __MANGANIS__
//! When the binary is built, the `dioxus asset!()` macro will emit its metadata into the __ASSETS__
//! symbols, which we process here. After reading the metadata directly from the executable, we then
//! hash it and write the hash directly into the binary file.
//!
@@ -23,7 +23,7 @@
//! can be found relative to the current exe. Unfortunately, on android, the `current_exe` path is wrong,
//! so the assets are resolved against the "asset root" - which is covered by the asset loader crate.
//!
//! Finding the __MANGANIS__ symbols is not quite straightforward when hotpatching, especially on WASM
//! Finding the __ASSETS__ symbols is not quite straightforward when hotpatching, especially on WASM
//! since we build and link the module as relocatable, which is not a stable WASM proposal. In this
//! implementation, we handle both the non-PIE *and* PIC cases which are rather bespoke to our whole
//! build system.
@@ -35,9 +35,9 @@ use std::{
use crate::Result;
use anyhow::{bail, Context};
use const_serialize::{ConstVec, SerializeConst};
use const_serialize::{serialize_const, ConstVec, SerializeConst};
use dioxus_cli_opt::AssetManifest;
use manganis::BundledAsset;
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
use pdb::FallibleIterator;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
@@ -45,24 +45,208 @@ use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
/// Extract all manganis symbols and their sections from the given object file.
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
file: &'b File<'a, R>,
) -> impl Iterator<Item = (Symbol<'a, 'b, R>, Section<'a, 'b, R>)> + 'b {
file.symbols()
.filter(|symbol| {
if let Ok(name) = symbol.name() {
looks_like_manganis_symbol(name)
} else {
false
}
})
.filter_map(move |symbol| {
) -> impl Iterator<Item = (ManganisVersion, Symbol<'a, 'b, R>, Section<'a, 'b, R>)> + 'b {
file.symbols().filter_map(move |symbol| {
let name = symbol.name().ok()?;
let version = looks_like_manganis_symbol(name)?;
let section_index = symbol.section_index()?;
let section = file.section_by_index(section_index).ok()?;
Some((symbol, section))
Some((version, symbol, section))
})
}
fn looks_like_manganis_symbol(name: &str) -> bool {
name.contains("__MANGANIS__")
#[derive(Copy, Clone)]
enum ManganisVersion {
/// The legacy version of the manganis format published with 0.7.0 and 0.7.1
Legacy,
/// The new version of the manganis format 0.7.2 onward
New,
}
impl ManganisVersion {
fn size(&self) -> usize {
match self {
ManganisVersion::Legacy => {
<manganis_core_07::BundledAsset as const_serialize_07::SerializeConst>::MEMORY_LAYOUT.size()
}
ManganisVersion::New => BundledAsset::MEMORY_LAYOUT.size(),
}
}
fn deserialize(&self, data: &[u8]) -> Option<BundledAsset> {
match self {
ManganisVersion::Legacy => {
let buffer = const_serialize_07::ConstReadBuffer::new(data);
let (_, legacy_asset) =
const_serialize_07::deserialize_const!(manganis_core_07::BundledAsset, buffer)?;
Some(legacy_asset_to_modern_asset(&legacy_asset))
}
ManganisVersion::New => {
let (_, asset) =
const_serialize::deserialize_const!(manganis_core::BundledAsset, data)?;
Some(asset)
}
}
}
fn serialize(&self, asset: &BundledAsset) -> Vec<u8> {
match self {
ManganisVersion::Legacy => {
let legacy_asset = modern_asset_to_legacy_asset(asset);
let buffer = const_serialize_07::serialize_const(
&legacy_asset,
const_serialize_07::ConstVec::new(),
);
buffer.as_ref().to_vec()
}
ManganisVersion::New => {
let buffer = serialize_const(asset, ConstVec::new());
buffer.as_ref().to_vec()
}
}
}
}
fn legacy_asset_to_modern_asset(
legacy_asset: &manganis_core_07::BundledAsset,
) -> manganis_core::BundledAsset {
let bundled_path = legacy_asset.bundled_path();
let absolute_path = legacy_asset.absolute_source_path();
let legacy_options = legacy_asset.options();
let add_hash = legacy_options.hash_suffix();
let options = match legacy_options.variant() {
manganis_core_07::AssetVariant::Image(image) => {
let format = match image.format() {
manganis_core_07::ImageFormat::Png => ImageFormat::Png,
manganis_core_07::ImageFormat::Jpg => ImageFormat::Jpg,
manganis_core_07::ImageFormat::Webp => ImageFormat::Webp,
manganis_core_07::ImageFormat::Avif => ImageFormat::Avif,
manganis_core_07::ImageFormat::Unknown => ImageFormat::Unknown,
};
let size = match image.size() {
manganis_core_07::ImageSize::Automatic => ImageSize::Automatic,
manganis_core_07::ImageSize::Manual { width, height } => {
ImageSize::Manual { width, height }
}
};
let preload = image.preloaded();
AssetOptions::image()
.with_format(format)
.with_size(size)
.with_preload(preload)
.with_hash_suffix(add_hash)
.into_asset_options()
}
manganis_core_07::AssetVariant::Folder(_) => AssetOptions::folder()
.with_hash_suffix(add_hash)
.into_asset_options(),
manganis_core_07::AssetVariant::Css(css) => AssetOptions::css()
.with_hash_suffix(add_hash)
.with_minify(css.minified())
.with_preload(css.preloaded())
.with_static_head(css.static_head())
.into_asset_options(),
manganis_core_07::AssetVariant::CssModule(css_module) => AssetOptions::css_module()
.with_hash_suffix(add_hash)
.with_minify(css_module.minified())
.with_preload(css_module.preloaded())
.into_asset_options(),
manganis_core_07::AssetVariant::Js(js) => AssetOptions::js()
.with_hash_suffix(add_hash)
.with_minify(js.minified())
.with_preload(js.preloaded())
.with_static_head(js.static_head())
.into_asset_options(),
_ => AssetOptions::builder()
.with_hash_suffix(add_hash)
.into_asset_options(),
};
BundledAsset::new(absolute_path, bundled_path, options)
}
fn modern_asset_to_legacy_asset(modern_asset: &BundledAsset) -> manganis_core_07::BundledAsset {
let bundled_path = modern_asset.bundled_path();
let absolute_path = modern_asset.absolute_source_path();
let legacy_options = modern_asset.options();
let add_hash = legacy_options.hash_suffix();
let options = match legacy_options.variant() {
AssetVariant::Image(image) => {
let format = match image.format() {
ImageFormat::Png => manganis_core_07::ImageFormat::Png,
ImageFormat::Jpg => manganis_core_07::ImageFormat::Jpg,
ImageFormat::Webp => manganis_core_07::ImageFormat::Webp,
ImageFormat::Avif => manganis_core_07::ImageFormat::Avif,
ImageFormat::Unknown => manganis_core_07::ImageFormat::Unknown,
};
let size = match image.size() {
ImageSize::Automatic => manganis_core_07::ImageSize::Automatic,
ImageSize::Manual { width, height } => {
manganis_core_07::ImageSize::Manual { width, height }
}
};
let preload = image.preloaded();
manganis_core_07::AssetOptions::image()
.with_format(format)
.with_size(size)
.with_preload(preload)
.with_hash_suffix(add_hash)
.into_asset_options()
}
AssetVariant::Folder(_) => manganis_core_07::AssetOptions::folder()
.with_hash_suffix(add_hash)
.into_asset_options(),
AssetVariant::Css(css) => manganis_core_07::AssetOptions::css()
.with_hash_suffix(add_hash)
.with_minify(css.minified())
.with_preload(css.preloaded())
.with_static_head(css.static_head())
.into_asset_options(),
AssetVariant::CssModule(css_module) => manganis_core_07::AssetOptions::css_module()
.with_hash_suffix(add_hash)
.with_minify(css_module.minified())
.with_preload(css_module.preloaded())
.into_asset_options(),
AssetVariant::Js(js) => manganis_core_07::AssetOptions::js()
.with_hash_suffix(add_hash)
.with_minify(js.minified())
.with_preload(js.preloaded())
.with_static_head(js.static_head())
.into_asset_options(),
_ => manganis_core_07::AssetOptions::builder()
.with_hash_suffix(add_hash)
.into_asset_options(),
};
manganis_core_07::BundledAsset::new(absolute_path, bundled_path, options)
}
fn looks_like_manganis_symbol(name: &str) -> Option<ManganisVersion> {
if name.contains("__MANGANIS__") {
Some(ManganisVersion::Legacy)
} else if name.contains("__ASSETS__") {
Some(ManganisVersion::New)
} else {
None
}
}
/// An asset offset in the binary
#[derive(Clone, Copy)]
struct ManganisSymbolOffset {
version: ManganisVersion,
offset: u64,
}
impl ManganisSymbolOffset {
fn new(version: ManganisVersion, offset: u64) -> Self {
Self { version, offset }
}
}
/// Find the offsets of any manganis symbols in the given file.
@@ -70,7 +254,7 @@ fn find_symbol_offsets<'a, R: ReadRef<'a>>(
path: &Path,
file_contents: &[u8],
file: &File<'a, R>,
) -> Result<Vec<u64>> {
) -> Result<Vec<ManganisSymbolOffset>> {
let pdb_file = find_pdb_file(path);
match file.format() {
@@ -118,7 +302,7 @@ fn find_pdb_file(path: &Path) -> Option<PathBuf> {
}
/// Find the offsets of any manganis symbols in a pdb file.
fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result<Vec<u64>> {
fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result<Vec<ManganisSymbolOffset>> {
let pdb_file_handle = std::fs::File::open(pdb_file)?;
let mut pdb_file = pdb::PDB::open(pdb_file_handle).context("Failed to open PDB file")?;
let Ok(Some(sections)) = pdb_file.sections() else {
@@ -142,26 +326,31 @@ fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result<Vec<u64>> {
};
let name = data.name.to_string();
if name.contains("__MANGANIS__") {
if let Some(version) = looks_like_manganis_symbol(&name) {
let section = sections
.get(rva.section as usize - 1)
.expect("Section index out of bounds");
addresses.push((section.pointer_to_raw_data + rva.offset) as u64);
addresses.push(ManganisSymbolOffset::new(
version,
(section.pointer_to_raw_data + rva.offset) as u64,
));
}
}
Ok(addresses)
}
/// Find the offsets of any manganis symbols in a native object file.
fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result<Vec<u64>> {
fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(
file: &File<'a, R>,
) -> Result<Vec<ManganisSymbolOffset>> {
let mut offsets = Vec::new();
for (symbol, section) in manganis_symbols(file) {
for (version, symbol, section) in manganis_symbols(file) {
let virtual_address = symbol.address();
let Some((section_range_start, _)) = section.file_range() else {
tracing::error!(
"Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range",
"Found __ASSETS__ symbol {:?} in section {}, but the section has no file range",
symbol.name(),
section.index()
);
@@ -172,7 +361,7 @@ fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result<
.try_into()
.expect("Virtual address should be greater than or equal to section address");
let file_offset = section_range_start + section_relative_address;
offsets.push(file_offset);
offsets.push(ManganisSymbolOffset::new(version, file_offset));
}
Ok(offsets)
@@ -198,7 +387,7 @@ fn eval_walrus_global_expr(module: &walrus::Module, expr: &walrus::ConstExpr) ->
fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
file_contents: &[u8],
file: &File<'a, R>,
) -> Result<Vec<u64>> {
) -> Result<Vec<ManganisSymbolOffset>> {
let Some(section) = file
.sections()
.find(|section| section.name() == Ok("<data>"))
@@ -259,9 +448,9 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
eval_walrus_global_expr(&module, &main_memory_offset).unwrap_or_default();
for export in module.exports.iter() {
if !looks_like_manganis_symbol(&export.name) {
let Some(version) = looks_like_manganis_symbol(&export.name) else {
continue;
}
};
let walrus::ExportItem::Global(global) = export.item else {
continue;
@@ -273,7 +462,7 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
let Some(virtual_address) = eval_walrus_global_expr(&module, &pointer) else {
tracing::error!(
"Found __MANGANIS__ symbol {:?} in WASM file, but the global expression could not be evaluated",
"Found __ASSETS__ symbol {:?} in WASM file, but the global expression could not be evaluated",
export.name
);
continue;
@@ -285,7 +474,7 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
.expect("Virtual address should be greater than or equal to section address");
let file_offset = data_start_offset + section_relative_address;
offsets.push(file_offset);
offsets.push(ManganisSymbolOffset::new(version, file_offset));
}
Ok(offsets)
@@ -311,15 +500,14 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
let mut assets = Vec::new();
// Read each asset from the data section using the offsets
for offset in offsets.iter().copied() {
for symbol in offsets.iter().copied() {
let version = symbol.version;
let offset = symbol.offset;
file.seek(std::io::SeekFrom::Start(offset))?;
let mut data_in_range = vec![0; BundledAsset::MEMORY_LAYOUT.size()];
let mut data_in_range = vec![0; version.size()];
file.read_exact(&mut data_in_range)?;
let buffer = const_serialize::ConstReadBuffer::new(&data_in_range);
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
{
if let Some(bundled_asset) = version.deserialize(&data_in_range) {
tracing::debug!(
"Found asset at offset {offset}: {:?}",
bundled_asset.absolute_source_path()
@@ -336,10 +524,10 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
.for_each(dioxus_cli_opt::add_hash_to_asset);
// Write back the assets to the binary file
for (offset, asset) in offsets.into_iter().zip(&assets) {
tracing::debug!("Writing asset to offset {offset}: {:?}", asset);
let new_data = ConstVec::new();
let new_data = const_serialize::serialize_const(asset, new_data);
for (symbol, asset) in offsets.into_iter().zip(&assets) {
let version = symbol.version;
let offset = symbol.offset;
let new_data = version.serialize(asset);
file.seek(std::io::SeekFrom::Start(offset))?;
// Write the modified binary data back to the file
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "const-serialize-macro"
version = { workspace = true }
version = "0.8.0-alpha.0"
authors = ["Evan Almloff"]
edition = "2021"
description = "A macro to derive const serialize"
+50 -23
View File
@@ -1,12 +1,12 @@
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, DeriveInput, LitInt};
use syn::{parse_macro_input, DeriveInput, LitInt, Path};
use syn::{parse_quote, Generics, WhereClause, WherePredicate};
fn add_bounds(where_clause: &mut Option<WhereClause>, generics: &Generics) {
fn add_bounds(where_clause: &mut Option<WhereClause>, generics: &Generics, krate: &Path) {
let bounds = generics.params.iter().filter_map(|param| match param {
syn::GenericParam::Type(ty) => {
Some::<WherePredicate>(parse_quote! { #ty: const_serialize::SerializeConst, })
Some::<WherePredicate>(parse_quote! { #ty: #krate::SerializeConst, })
}
syn::GenericParam::Lifetime(_) => None,
syn::GenericParam::Const(_) => None,
@@ -19,10 +19,33 @@ fn add_bounds(where_clause: &mut Option<WhereClause>, generics: &Generics) {
}
/// Derive the const serialize trait for a struct
#[proc_macro_derive(SerializeConst)]
pub fn derive_parse(input: TokenStream) -> TokenStream {
#[proc_macro_derive(SerializeConst, attributes(const_serialize))]
pub fn derive_parse(raw_input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
let input = parse_macro_input!(raw_input as DeriveInput);
let krate = input.attrs.iter().find_map(|attr| {
attr.path()
.is_ident("const_serialize")
.then(|| {
let mut path = None;
if let Err(err) = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("crate") {
let ident: Path = meta.value()?.parse()?;
path = Some(ident);
}
Ok(())
}) {
return Some(Err(err));
}
path.map(Ok)
})
.flatten()
});
let krate = match krate {
Some(Ok(path)) => path,
Some(Err(err)) => return err.into_compile_error().into(),
None => parse_quote! { const_serialize },
};
match input.data {
syn::Data::Struct(data) => match data.fields {
@@ -30,7 +53,7 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
let ty = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut where_clause = where_clause.cloned();
add_bounds(&mut where_clause, &input.generics);
add_bounds(&mut where_clause, &input.generics, &krate);
let field_names = data.fields.iter().enumerate().map(|(i, field)| {
field
.ident
@@ -43,13 +66,14 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
});
let field_types = data.fields.iter().map(|field| &field.ty);
quote! {
unsafe impl #impl_generics const_serialize::SerializeConst for #ty #ty_generics #where_clause {
const MEMORY_LAYOUT: const_serialize::Layout = const_serialize::Layout::Struct(const_serialize::StructLayout::new(
unsafe impl #impl_generics #krate::SerializeConst for #ty #ty_generics #where_clause {
const MEMORY_LAYOUT: #krate::Layout = #krate::Layout::Struct(#krate::StructLayout::new(
std::mem::size_of::<Self>(),
&[#(
const_serialize::StructFieldLayout::new(
#krate::StructFieldLayout::new(
stringify!(#field_names),
std::mem::offset_of!(#ty, #field_names),
<#field_types as const_serialize::SerializeConst>::MEMORY_LAYOUT,
<#field_types as #krate::SerializeConst>::MEMORY_LAYOUT,
),
)*],
));
@@ -60,10 +84,10 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
let ty = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut where_clause = where_clause.cloned();
add_bounds(&mut where_clause, &input.generics);
add_bounds(&mut where_clause, &input.generics, &krate);
quote! {
unsafe impl #impl_generics const_serialize::SerializeConst for #ty #ty_generics #where_clause {
const MEMORY_LAYOUT: const_serialize::Layout = const_serialize::Layout::Struct(const_serialize::StructLayout::new(
unsafe impl #impl_generics #krate::SerializeConst for #ty #ty_generics #where_clause {
const MEMORY_LAYOUT: #krate::Layout = #krate::Layout::Struct(#krate::StructLayout::new(
std::mem::size_of::<Self>(),
&[],
));
@@ -137,7 +161,7 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
let ty = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut where_clause = where_clause.cloned();
add_bounds(&mut where_clause, &input.generics);
add_bounds(&mut where_clause, &input.generics, &krate);
let mut last_discriminant = None;
let variants = data.variants.iter().map(|variant| {
let discriminant = variant
@@ -151,6 +175,7 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
}
});
last_discriminant = Some(discriminant.clone());
let variant_name = &variant.ident;
let field_names = variant.fields.iter().enumerate().map(|(i, field)| {
field
.ident
@@ -162,17 +187,19 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
quote! {
{
#[allow(unused)]
#[derive(const_serialize::SerializeConst)]
#[derive(#krate::SerializeConst)]
#[const_serialize(crate = #krate)]
#[repr(C)]
struct VariantStruct #generics {
#(
#field_names: #field_types,
)*
}
const_serialize::EnumVariant::new(
#krate::EnumVariant::new(
stringify!(#variant_name),
#discriminant as u32,
match VariantStruct::MEMORY_LAYOUT {
const_serialize::Layout::Struct(layout) => layout,
match <VariantStruct #generics as #krate::SerializeConst>::MEMORY_LAYOUT {
#krate::Layout::Struct(layout) => layout,
_ => panic!("VariantStruct::MEMORY_LAYOUT must be a struct"),
},
::std::mem::align_of::<VariantStruct>(),
@@ -181,14 +208,14 @@ pub fn derive_parse(input: TokenStream) -> TokenStream {
}
});
quote! {
unsafe impl #impl_generics const_serialize::SerializeConst for #ty #ty_generics #where_clause {
const MEMORY_LAYOUT: const_serialize::Layout = const_serialize::Layout::Enum(const_serialize::EnumLayout::new(
unsafe impl #impl_generics #krate::SerializeConst for #ty #ty_generics #where_clause {
const MEMORY_LAYOUT: #krate::Layout = #krate::Layout::Enum(#krate::EnumLayout::new(
::std::mem::size_of::<Self>(),
const_serialize::PrimitiveLayout::new(
#krate::PrimitiveLayout::new(
#discriminant_size as usize,
),
{
const DATA: &'static [const_serialize::EnumVariant] = &[
const DATA: &'static [#krate::EnumVariant] = &[
#(
#variants,
)*
+5 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "const-serialize"
version = { workspace = true }
version = "0.8.0"
authors = ["Evan Almloff"]
edition = "2021"
description = "A serialization framework that works in const contexts"
@@ -8,11 +8,12 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/dioxuslabs/dioxus"
homepage = "https://dioxuslabs.com/learn/0.5/getting_started"
keywords = ["const", "serialize"]
rust-version = "1.80.0"
rust-version = "1.83.0"
[dependencies]
const-serialize-macro = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
const-serialize-07 = { workspace = true, optional = true }
[dev-dependencies]
const-serialize = { path = "." }
@@ -21,3 +22,5 @@ rand = { workspace = true }
[features]
test-big-endian = []
serde = ["dep:serde"]
# Enables serialization of the ConstStr with the older const-serialize-0.7 crate
const-serialize-07 = ["dep:const-serialize-07"]
+2 -2
View File
@@ -29,7 +29,7 @@ const {
}; 3];
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
let buf = buf.read();
let buf = buf.as_ref();
let (buf, deserialized) = match deserialize_const!([Struct; 3], buf) {
Some(data) => data,
None => panic!("data mismatch"),
@@ -54,4 +54,4 @@ The rust [nomicon](https://doc.rust-lang.org/nomicon/data.html) defines the memo
- Only constant sized types are supported. This means that you can't serialize a type like `Vec<T>`. These types are difficult to create in const contexts in general
- Only types with a well defined memory layout are supported (see <https://github.com/rust-lang/rfcs/pull/3727> and <https://onevariable.com/blog/pods-from-scratch>). `repr(Rust)` enums don't have a well defined layout, so they are not supported. `repr(C, u8)` enums can be used instead
- Const rust does not support mutable references or points, so this crate leans heavily on function data structures for data processing.
- Const rust does not support mutable references or points, so this crate leans heavily on functional data structures for data processing.
+64
View File
@@ -0,0 +1,64 @@
use crate::*;
/// The layout for a constant sized array. The array layout is just a length and an item layout.
#[derive(Debug, Copy, Clone)]
pub struct ArrayLayout {
pub(crate) len: usize,
pub(crate) item_layout: &'static Layout,
}
impl ArrayLayout {
/// Create a new array layout
pub const fn new(len: usize, item_layout: &'static Layout) -> Self {
Self { len, item_layout }
}
}
unsafe impl<const N: usize, T: SerializeConst> SerializeConst for [T; N] {
const MEMORY_LAYOUT: Layout = Layout::Array(ArrayLayout {
len: N,
item_layout: &T::MEMORY_LAYOUT,
});
}
/// Serialize a constant sized array that is stored at the pointer passed in
pub(crate) const unsafe fn serialize_const_array(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &ArrayLayout,
) -> ConstVec<u8> {
let len = layout.len;
let mut i = 0;
to = write_array(to, len);
while i < len {
let field = ptr.wrapping_byte_offset((i * layout.item_layout.size()) as _);
to = serialize_const_ptr(field, to, layout.item_layout);
i += 1;
}
to
}
/// Deserialize an array type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
pub(crate) const fn deserialize_const_array<'a>(
from: &'a [u8],
layout: &ArrayLayout,
mut out: &mut [MaybeUninit<u8>],
) -> Option<&'a [u8]> {
let item_layout = layout.item_layout;
let Ok((_, mut from)) = take_array(from) else {
return None;
};
let mut i = 0;
while i < layout.len {
let Some(new_from) = deserialize_const_ptr(from, item_layout, out) else {
return None;
};
let Some((_, item_out)) = out.split_at_mut_checked(item_layout.size()) else {
return None;
};
out = item_out;
from = new_from;
i += 1;
}
Some(from)
}
+597
View File
@@ -0,0 +1,597 @@
//! Const serialization utilities for the CBOR data format.
//!
//! ## Overview of the format
//!
//! Const serialize only supports a subset of the CBOR format, specifically the major types:
//! - UnsignedInteger
//! - NegativeInteger
//! - Bytes
//! - String
//! - Array
//!
//! Each item in CBOR starts with a leading byte, which determines the type of the item and additional information.
//! The additional information is encoded in the lower 5 bits of the leading byte and generally indicates either a
//! small number or how many of the next bytes are part of the first number.
//!
//! Resources:
//! The spec: <https://www.rfc-editor.org/rfc/rfc8949.html>
//! A playground to check examples against: <https://cbor.me/>
use crate::ConstVec;
/// Each item in CBOR starts with a leading byte, which determines the type of the item and additional information.
///
/// The first 3 bits of the leading byte are the major type, which indicates the type of the item.
#[repr(u8)]
#[derive(PartialEq)]
enum MajorType {
/// An unsigned integer in the range 0..2^64. The value of the number is encoded in the remaining bits of the leading byte and any additional bytes.
UnsignedInteger = 0,
/// An unsigned integer in the range -2^64..-1. The value of the number is encoded in the remaining bits of the leading byte and any additional bytes
NegativeInteger = 1,
/// A byte sequence. The number of bytes in the sequence is encoded in the remaining bits of the leading byte and any additional bytes.
Bytes = 2,
/// A text sequence. The number of bytes in the sequence is encoded in the remaining bits of the leading byte and any additional bytes.
Text = 3,
/// A dynamically sized array of non-uniform data items. The number of items in the array is encoded in the remaining bits of the leading byte and any additional bytes.
Array = 4,
/// A map of pairs of data items. The first item in each pair is the key and the second item is the value. The number of items in the array is encoded in the remaining bits of the leading byte and any additional bytes.
Map = 5,
/// Tagged values - not supported
Tagged = 6,
/// Floating point values - not supported
Float = 7,
}
impl MajorType {
/// The bitmask for the major type in the leading byte
const MASK: u8 = 0b0001_1111;
const fn from_byte(byte: u8) -> Self {
match byte >> 5 {
0 => MajorType::UnsignedInteger,
1 => MajorType::NegativeInteger,
2 => MajorType::Bytes,
3 => MajorType::Text,
4 => MajorType::Array,
5 => MajorType::Map,
6 => MajorType::Tagged,
7 => MajorType::Float,
_ => panic!("Invalid major type"),
}
}
}
/// Get the length of the item in bytes without deserialization.
const fn item_length(bytes: &[u8]) -> Result<usize, ()> {
let [head, rest @ ..] = bytes else {
return Err(());
};
let major = MajorType::from_byte(*head);
let additional_information = *head & MajorType::MASK;
let length_of_item = match major {
// The length of the number is the total of:
// - The length of the number (which may be 0 if the number is encoded in additional information)
MajorType::UnsignedInteger | MajorType::NegativeInteger => {
get_length_of_number(additional_information) as usize
}
// The length of the text or bytes is the total of:
// - The length of the number that denotes the length of the text or bytes
// - The length of the text or bytes themselves
MajorType::Text | MajorType::Bytes => {
let length_of_number = get_length_of_number(additional_information);
let Ok((length_of_bytes, _)) =
grab_u64_with_byte_length(rest, length_of_number, additional_information)
else {
return Err(());
};
length_of_number as usize + length_of_bytes as usize
}
// The length of the map is the total of:
// - The length of the number that denotes the number of items
// - The length of the pairs of items themselves
MajorType::Array | MajorType::Map => {
let length_of_number = get_length_of_number(additional_information);
let Ok((length_of_items, _)) =
grab_u64_with_byte_length(rest, length_of_number, additional_information)
else {
return Err(());
};
let mut total_length = length_of_number as usize;
let mut items_left = length_of_items * if let MajorType::Map = major { 2 } else { 1 };
while items_left > 0 {
let Some((_, after)) = rest.split_at_checked(total_length) else {
return Err(());
};
let Ok(item_length) = item_length(after) else {
return Err(());
};
total_length += item_length;
items_left -= 1;
}
total_length
}
_ => return Err(()),
};
let length_of_head = 1;
Ok(length_of_head + length_of_item)
}
/// Read a number from the buffer, returning the number and the remaining bytes.
pub(crate) const fn take_number(bytes: &[u8]) -> Result<(i64, &[u8]), ()> {
let [head, rest @ ..] = bytes else {
return Err(());
};
let major = MajorType::from_byte(*head);
let additional_information = *head & MajorType::MASK;
match major {
MajorType::UnsignedInteger => {
let Ok((number, rest)) = grab_u64(rest, additional_information) else {
return Err(());
};
Ok((number as i64, rest))
}
MajorType::NegativeInteger => {
let Ok((number, rest)) = grab_u64(rest, additional_information) else {
return Err(());
};
Ok((-(1 + number as i64), rest))
}
_ => Err(()),
}
}
/// Write a number to the buffer
pub(crate) const fn write_number<const MAX_SIZE: usize>(
vec: ConstVec<u8, MAX_SIZE>,
number: i64,
) -> ConstVec<u8, MAX_SIZE> {
match number {
0.. => write_major_type_and_u64(vec, MajorType::UnsignedInteger, number as u64),
..0 => write_major_type_and_u64(vec, MajorType::NegativeInteger, (-(number + 1)) as u64),
}
}
/// Write the major type along with a number to the buffer. The first byte
/// contains both the major type and the additional information which contains
/// either the number itself or the number of extra bytes the number occupies.
const fn write_major_type_and_u64<const MAX_SIZE: usize>(
vec: ConstVec<u8, MAX_SIZE>,
major: MajorType,
number: u64,
) -> ConstVec<u8, MAX_SIZE> {
let major = (major as u8) << 5;
match number {
// For numbers less than 24, store the number in the lower bits
// of the first byte
0..24 => {
let additional_information = number as u8;
let byte = major | additional_information;
vec.push(byte)
}
// For larger numbers, store the number of extra bytes the number occupies
24.. => {
let log2_additional_bytes = log2_bytes_for_number(number);
let additional_bytes = 1 << log2_additional_bytes;
let additional_information = log2_additional_bytes + 24;
let byte = major | additional_information;
let mut vec = vec.push(byte);
let mut byte = 0;
while byte < additional_bytes {
vec = vec.push((number >> ((additional_bytes - byte - 1) * 8)) as u8);
byte += 1;
}
vec
}
}
}
/// Find the number of bytes required to store a number and return the log2 of the number of bytes.
/// This is the number stored in the additional information field if the number is more than 24.
const fn log2_bytes_for_number(number: u64) -> u8 {
let required_bytes = ((64 - number.leading_zeros()).div_ceil(8)) as u8;
#[allow(clippy::match_overlapping_arm)]
match required_bytes {
..=1 => 0,
..=2 => 1,
..=4 => 2,
_ => 3,
}
}
/// Take bytes from a slice and return the bytes and the remaining slice.
pub(crate) const fn take_bytes(bytes: &[u8]) -> Result<(&[u8], &[u8]), ()> {
let [head, rest @ ..] = bytes else {
return Err(());
};
let major = MajorType::from_byte(*head);
let additional_information = *head & MajorType::MASK;
if let MajorType::Bytes = major {
take_bytes_from(rest, additional_information)
} else {
Err(())
}
}
/// Write bytes to a buffer and return the new buffer.
pub(crate) const fn write_bytes<const MAX_SIZE: usize>(
vec: ConstVec<u8, MAX_SIZE>,
bytes: &[u8],
) -> ConstVec<u8, MAX_SIZE> {
let vec = write_major_type_and_u64(vec, MajorType::Bytes, bytes.len() as u64);
vec.extend(bytes)
}
/// Take a string from a buffer and return the string and the remaining buffer.
pub(crate) const fn take_str(bytes: &[u8]) -> Result<(&str, &[u8]), ()> {
let [head, rest @ ..] = bytes else {
return Err(());
};
let major = MajorType::from_byte(*head);
let additional_information = *head & MajorType::MASK;
if let MajorType::Text = major {
let Ok((bytes, rest)) = take_bytes_from(rest, additional_information) else {
return Err(());
};
let Ok(string) = std::str::from_utf8(bytes) else {
return Err(());
};
Ok((string, rest))
} else {
Err(())
}
}
/// Write a string to a buffer and return the new buffer.
pub(crate) const fn write_str<const MAX_SIZE: usize>(
vec: ConstVec<u8, MAX_SIZE>,
string: &str,
) -> ConstVec<u8, MAX_SIZE> {
let vec = write_major_type_and_u64(vec, MajorType::Text, string.len() as u64);
vec.extend(string.as_bytes())
}
/// Take the length and header of an array from a buffer and return the length and the remaining buffer.
/// You must loop over the elements of the array and parse them outside of this method.
pub(crate) const fn take_array(bytes: &[u8]) -> Result<(usize, &[u8]), ()> {
let [head, rest @ ..] = bytes else {
return Err(());
};
let major = MajorType::from_byte(*head);
let additional_information = *head & MajorType::MASK;
if let MajorType::Array = major {
let Ok((length, rest)) = take_len_from(rest, additional_information) else {
return Err(());
};
Ok((length as usize, rest))
} else {
Err(())
}
}
/// Write the header and length of an array.
pub(crate) const fn write_array<const MAX_SIZE: usize>(
vec: ConstVec<u8, MAX_SIZE>,
len: usize,
) -> ConstVec<u8, MAX_SIZE> {
write_major_type_and_u64(vec, MajorType::Array, len as u64)
}
/// Write the header and length of a map.
pub(crate) const fn write_map<const MAX_SIZE: usize>(
vec: ConstVec<u8, MAX_SIZE>,
len: usize,
) -> ConstVec<u8, MAX_SIZE> {
// We write 2 * len as the length of the map because each key-value pair is a separate entry.
write_major_type_and_u64(vec, MajorType::Map, len as u64)
}
/// Write the key of a map entry.
pub(crate) const fn write_map_key<const MAX_SIZE: usize>(
value: ConstVec<u8, MAX_SIZE>,
key: &str,
) -> ConstVec<u8, MAX_SIZE> {
write_str(value, key)
}
/// Take a map from the byte slice and return the map reference and the remaining bytes.
pub(crate) const fn take_map<'a>(bytes: &'a [u8]) -> Result<(MapRef<'a>, &'a [u8]), ()> {
let [head, rest @ ..] = bytes else {
return Err(());
};
let major = MajorType::from_byte(*head);
let additional_information = *head & MajorType::MASK;
if let MajorType::Map = major {
let Ok((length, rest)) = take_len_from(rest, additional_information) else {
return Err(());
};
let mut after_map = rest;
let mut items_left = length * 2;
while items_left > 0 {
// Skip the value
let Ok(len) = item_length(after_map) else {
return Err(());
};
let Some((_, rest)) = after_map.split_at_checked(len) else {
return Err(());
};
after_map = rest;
items_left -= 1;
}
Ok((MapRef::new(rest, length as usize), after_map))
} else {
Err(())
}
}
/// A reference to a CBOR map.
pub(crate) struct MapRef<'a> {
/// The bytes of the map.
pub(crate) bytes: &'a [u8],
/// The length of the map.
pub(crate) len: usize,
}
impl<'a> MapRef<'a> {
/// Create a new map reference.
const fn new(bytes: &'a [u8], len: usize) -> Self {
Self { bytes, len }
}
/// Find a key in the map and return the buffer associated with it.
pub(crate) const fn find(&self, key: &str) -> Result<Option<&[u8]>, ()> {
let mut bytes = self.bytes;
let mut items_left = self.len;
while items_left > 0 {
let Ok((str, rest)) = take_str(bytes) else {
return Err(());
};
if str_eq(key, str) {
return Ok(Some(rest));
}
// Skip the value associated with the key we don't care about
let Ok(len) = item_length(rest) else {
return Err(());
};
let Some((_, rest)) = rest.split_at_checked(len) else {
return Err(());
};
bytes = rest;
items_left -= 1;
}
Ok(None)
}
}
/// Compare two strings for equality at compile time.
pub(crate) const fn str_eq(a: &str, b: &str) -> bool {
let a_bytes = a.as_bytes();
let b_bytes = b.as_bytes();
let a_len = a_bytes.len();
let b_len = b_bytes.len();
if a_len != b_len {
return false;
}
let mut index = 0;
while index < a_len {
if a_bytes[index] != b_bytes[index] {
return false;
}
index += 1;
}
true
}
/// Take the length from the additional information byte and return it along with the remaining bytes.
const fn take_len_from(rest: &[u8], additional_information: u8) -> Result<(u64, &[u8]), ()> {
match additional_information {
// If additional_information < 24, the argument's value is the value of the additional information.
0..24 => Ok((additional_information as u64, rest)),
// If additional_information is between 24 and 28, the argument's value is held in the n following bytes.
24..28 => {
let Ok((number, rest)) = grab_u64(rest, additional_information) else {
return Err(());
};
Ok((number, rest))
}
_ => Err(()),
}
}
/// Take a list of bytes from the byte slice and the additional information byte
/// and return the bytes and the remaining bytes.
pub(crate) const fn take_bytes_from(
rest: &[u8],
additional_information: u8,
) -> Result<(&[u8], &[u8]), ()> {
let Ok((number, rest)) = grab_u64(rest, additional_information) else {
return Err(());
};
let Some((bytes, rest)) = rest.split_at_checked(number as usize) else {
return Err(());
};
Ok((bytes, rest))
}
/// Find the length of the number based on the additional information byte.
const fn get_length_of_number(additional_information: u8) -> u8 {
match additional_information {
0..24 => 0,
24..28 => 1 << (additional_information - 24),
_ => 0,
}
}
/// Read a u64 from the byte slice and the additional information byte.
const fn grab_u64(rest: &[u8], additional_information: u8) -> Result<(u64, &[u8]), ()> {
grab_u64_with_byte_length(
rest,
get_length_of_number(additional_information),
additional_information,
)
}
/// Read a u64 from the byte slice and the additional information byte along with the byte length.
const fn grab_u64_with_byte_length(
mut rest: &[u8],
byte_length: u8,
additional_information: u8,
) -> Result<(u64, &[u8]), ()> {
match byte_length {
0 => Ok((additional_information as u64, rest)),
n => {
let mut value = 0;
let mut count = 0;
while count < n {
let [next, remaining @ ..] = rest else {
return Err(());
};
value = (value << 8) | *next as u64;
rest = remaining;
count += 1;
}
Ok((value, rest))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_byte() {
for byte in 0..=255 {
let bytes = if byte < 24 { [byte, 0] } else { [24, byte] };
let (item, _) = take_number(&bytes).unwrap();
assert_eq!(item, byte as _);
}
for byte in 1..=255 {
let bytes = if byte < 24 {
[(byte - 1) | 0b0010_0000, 0]
} else {
[0b0010_0000 | 24, byte - 1]
};
let (item, _) = take_number(&bytes).unwrap();
assert_eq!(item, -(byte as i64));
}
}
#[test]
fn test_byte_roundtrip() {
for byte in 0..=255 {
let vec = write_number(ConstVec::new(), byte as _);
println!("{vec:?}");
let (item, _) = take_number(vec.as_ref()).unwrap();
assert_eq!(item, byte as _);
}
for byte in 0..=255 {
let vec = write_number(ConstVec::new(), -(byte as i64));
let (item, _) = take_number(vec.as_ref()).unwrap();
assert_eq!(item, -(byte as i64));
}
}
#[test]
fn test_number_roundtrip() {
for _ in 0..100 {
let value = rand::random::<i64>();
let vec = write_number(ConstVec::new(), value);
let (item, _) = take_number(vec.as_ref()).unwrap();
assert_eq!(item, value);
}
}
#[test]
fn test_bytes_roundtrip() {
for _ in 0..100 {
let len = (rand::random::<u8>() % 100) as usize;
let bytes = rand::random::<[u8; 100]>();
let vec = write_bytes(ConstVec::new(), &bytes[..len]);
let (item, _) = take_bytes(vec.as_ref()).unwrap();
assert_eq!(item, &bytes[..len]);
}
}
#[test]
fn test_array_roundtrip() {
for _ in 0..100 {
let len = (rand::random::<u8>() % 100) as usize;
let mut vec = write_array(ConstVec::new(), len);
for i in 0..len {
vec = write_number(vec, i as _);
}
let (len, mut remaining) = take_array(vec.as_ref()).unwrap();
for i in 0..len {
let (item, rest) = take_number(remaining).unwrap();
remaining = rest;
assert_eq!(item, i as i64);
}
}
}
#[test]
fn test_map_roundtrip() {
use rand::prelude::SliceRandom;
for _ in 0..100 {
let len = (rand::random::<u8>() % 10) as usize;
let mut vec = write_map(ConstVec::new(), len);
let mut random_order_indexes = (0..len).collect::<Vec<_>>();
random_order_indexes.shuffle(&mut rand::rng());
for &i in &random_order_indexes {
vec = write_map_key(vec, &i.to_string());
vec = write_number(vec, i as _);
}
println!("len: {}", len);
println!("Map: {:?}", vec);
let (map, remaining) = take_map(vec.as_ref()).unwrap();
println!("remaining: {:?}", remaining);
assert!(remaining.is_empty());
for i in 0..len {
let key = i.to_string();
let key_location = map
.find(&key)
.expect("encoding is valid")
.expect("key exists");
let (value, _) = take_number(key_location).unwrap();
assert_eq!(value, i as i64);
}
}
}
#[test]
fn test_item_length_str() {
#[rustfmt::skip]
let input = [
/* text(1) */ 0x61,
/* "1" */ 0x31,
/* text(1) */ 0x61,
/* "1" */ 0x31,
];
let Ok(length) = item_length(&input) else {
panic!("Failed to calculate length");
};
assert_eq!(length, 2);
}
#[test]
fn test_item_length_map() {
#[rustfmt::skip]
let input = [
/* map(1) */ 0xA1,
/* text(1) */ 0x61,
/* "A" */ 0x41,
/* map(2) */ 0xA2,
/* text(3) */ 0x63,
/* "one" */ 0x6F, 0x6E, 0x65,
/* unsigned(286331153) */ 0x1A, 0x11, 0x11, 0x11, 0x11,
/* text(3) */ 0x63,
/* "two" */ 0x74, 0x77, 0x6F,
/* unsigned(34) */ 0x18, 0x22,
];
let Ok(length) = item_length(&input) else {
panic!("Failed to calculate length");
};
assert_eq!(length, input.len());
}
}
+135
View File
@@ -0,0 +1,135 @@
use crate::*;
/// Serialize an enum that is stored at the pointer passed in
pub(crate) const unsafe fn serialize_const_enum(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &EnumLayout,
) -> ConstVec<u8> {
let byte_ptr = ptr as *const u8;
let discriminant = layout.discriminant.read(byte_ptr);
let mut i = 0;
while i < layout.variants.len() {
// If the variant is the discriminated one, serialize it
let EnumVariant {
tag, name, data, ..
} = &layout.variants[i];
if discriminant == *tag {
to = write_map(to, 1);
to = write_map_key(to, name);
let data_ptr = ptr.wrapping_byte_offset(layout.variants_offset as _);
to = serialize_const_struct(data_ptr, to, data);
break;
}
i += 1;
}
to
}
/// The layout for an enum. The enum layout is just a discriminate size and a tag layout.
#[derive(Debug, Copy, Clone)]
pub struct EnumLayout {
pub(crate) size: usize,
discriminant: PrimitiveLayout,
variants_offset: usize,
variants: &'static [EnumVariant],
}
impl EnumLayout {
/// Create a new enum layout
pub const fn new(
size: usize,
discriminant: PrimitiveLayout,
variants: &'static [EnumVariant],
) -> Self {
let mut max_align = 1;
let mut i = 0;
while i < variants.len() {
let EnumVariant { align, .. } = &variants[i];
if *align > max_align {
max_align = *align;
}
i += 1;
}
let variants_offset_raw = discriminant.size;
let padding = (max_align - (variants_offset_raw % max_align)) % max_align;
let variants_offset = variants_offset_raw + padding;
assert!(variants_offset % max_align == 0);
Self {
size,
discriminant,
variants_offset,
variants,
}
}
}
/// The layout for an enum variant. The enum variant layout is just a struct layout with a tag and alignment.
#[derive(Debug, Copy, Clone)]
pub struct EnumVariant {
name: &'static str,
// Note: tags may not be sequential
tag: u32,
data: StructLayout,
align: usize,
}
impl EnumVariant {
/// Create a new enum variant layout
pub const fn new(name: &'static str, tag: u32, data: StructLayout, align: usize) -> Self {
Self {
name,
tag,
data,
align,
}
}
}
/// Deserialize an enum type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
pub(crate) const fn deserialize_const_enum<'a>(
from: &'a [u8],
layout: &EnumLayout,
out: &mut [MaybeUninit<u8>],
) -> Option<&'a [u8]> {
// First, deserialize the map
let Ok((map, remaining)) = take_map(from) else {
return None;
};
// Then get the only field which is the tag
let Ok((deserilized_name, from)) = take_str(map.bytes) else {
return None;
};
// Then, deserialize the variant
let mut i = 0;
let mut matched_variant = false;
while i < layout.variants.len() {
// If the variant is the discriminated one, deserialize it
let EnumVariant {
name, data, tag, ..
} = &layout.variants[i];
if str_eq(deserilized_name, name) {
layout.discriminant.write(*tag, out);
let Some((_, out)) = out.split_at_mut_checked(layout.variants_offset) else {
return None;
};
if deserialize_const_struct(from, data, out).is_none() {
return None;
}
matched_variant = true;
break;
}
i += 1;
}
if !matched_variant {
return None;
}
Some(remaining)
}
+47 -781
View File
@@ -1,126 +1,32 @@
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
use std::{char, mem::MaybeUninit};
use std::mem::MaybeUninit;
mod const_buffers;
mod const_vec;
pub use const_buffers::ConstReadBuffer;
mod cbor;
mod const_vec;
mod r#enum;
pub use r#enum::*;
mod r#struct;
pub use r#struct::*;
mod primitive;
pub use primitive::*;
mod list;
pub use list::*;
mod array;
pub use array::*;
mod str;
pub use str::*;
pub use const_serialize_macro::SerializeConst;
pub use const_vec::ConstVec;
/// Plain old data for a field. Stores the offset of the field in the struct and the layout of the field.
#[derive(Debug, Copy, Clone)]
pub struct StructFieldLayout {
offset: usize,
layout: Layout,
}
impl StructFieldLayout {
/// Create a new struct field layout
pub const fn new(offset: usize, layout: Layout) -> Self {
Self { offset, layout }
}
}
/// Layout for a struct. The struct layout is just a list of fields with offsets
#[derive(Debug, Copy, Clone)]
pub struct StructLayout {
size: usize,
data: &'static [StructFieldLayout],
}
impl StructLayout {
/// Create a new struct layout
pub const fn new(size: usize, data: &'static [StructFieldLayout]) -> Self {
Self { size, data }
}
}
/// The layout for an enum. The enum layout is just a discriminate size and a tag layout.
#[derive(Debug, Copy, Clone)]
pub struct EnumLayout {
size: usize,
discriminant: PrimitiveLayout,
variants_offset: usize,
variants: &'static [EnumVariant],
}
impl EnumLayout {
/// Create a new enum layout
pub const fn new(
size: usize,
discriminant: PrimitiveLayout,
variants: &'static [EnumVariant],
) -> Self {
let mut max_align = 1;
let mut i = 0;
while i < variants.len() {
let EnumVariant { align, .. } = &variants[i];
if *align > max_align {
max_align = *align;
}
i += 1;
}
let variants_offset_raw = discriminant.size;
let padding = (max_align - (variants_offset_raw % max_align)) % max_align;
let variants_offset = variants_offset_raw + padding;
assert!(variants_offset % max_align == 0);
Self {
size,
discriminant,
variants_offset,
variants,
}
}
}
/// The layout for an enum variant. The enum variant layout is just a struct layout with a tag and alignment.
#[derive(Debug, Copy, Clone)]
pub struct EnumVariant {
// Note: tags may not be sequential
tag: u32,
data: StructLayout,
align: usize,
}
impl EnumVariant {
/// Create a new enum variant layout
pub const fn new(tag: u32, data: StructLayout, align: usize) -> Self {
Self { tag, data, align }
}
}
/// The layout for a constant sized array. The array layout is just a length and an item layout.
#[derive(Debug, Copy, Clone)]
pub struct ListLayout {
len: usize,
item_layout: &'static Layout,
}
impl ListLayout {
/// Create a new list layout
pub const fn new(len: usize, item_layout: &'static Layout) -> Self {
Self { len, item_layout }
}
}
/// The layout for a primitive type. The bytes will be reversed if the target is big endian.
#[derive(Debug, Copy, Clone)]
pub struct PrimitiveLayout {
size: usize,
}
impl PrimitiveLayout {
/// Create a new primitive layout
pub const fn new(size: usize) -> Self {
Self { size }
}
}
use crate::cbor::{
str_eq, take_array, take_bytes, take_map, take_number, take_str, write_array, write_bytes,
write_map, write_map_key, write_number,
};
/// The layout for a type. This layout defines a sequence of locations and reversed or not bytes. These bytes will be copied from during serialization and copied into during deserialization.
#[derive(Debug, Copy, Clone)]
@@ -129,10 +35,12 @@ pub enum Layout {
Enum(EnumLayout),
/// A struct layout
Struct(StructLayout),
/// A list layout
List(ListLayout),
/// An array layout
Array(ArrayLayout),
/// A primitive layout
Primitive(PrimitiveLayout),
/// A dynamically sized list layout
List(ListLayout),
}
impl Layout {
@@ -141,7 +49,8 @@ impl Layout {
match self {
Layout::Enum(layout) => layout.size,
Layout::Struct(layout) => layout.size,
Layout::List(layout) => layout.len * layout.item_layout.size(),
Layout::Array(layout) => layout.len * layout.item_layout.size(),
Layout::List(layout) => layout.size,
Layout::Primitive(layout) => layout.size,
}
}
@@ -158,533 +67,16 @@ pub unsafe trait SerializeConst: Sized {
const _ASSERT: () = assert!(Self::MEMORY_LAYOUT.size() == std::mem::size_of::<Self>());
}
macro_rules! impl_serialize_const {
($type:ty) => {
unsafe impl SerializeConst for $type {
const MEMORY_LAYOUT: Layout = Layout::Primitive(PrimitiveLayout {
size: std::mem::size_of::<$type>(),
});
}
};
}
impl_serialize_const!(u8);
impl_serialize_const!(u16);
impl_serialize_const!(u32);
impl_serialize_const!(u64);
impl_serialize_const!(i8);
impl_serialize_const!(i16);
impl_serialize_const!(i32);
impl_serialize_const!(i64);
impl_serialize_const!(bool);
impl_serialize_const!(f32);
impl_serialize_const!(f64);
unsafe impl<const N: usize, T: SerializeConst> SerializeConst for [T; N] {
const MEMORY_LAYOUT: Layout = Layout::List(ListLayout {
len: N,
item_layout: &T::MEMORY_LAYOUT,
});
}
macro_rules! impl_serialize_const_tuple {
($($generic:ident: $generic_number:expr),*) => {
impl_serialize_const_tuple!(@impl ($($generic,)*) = $($generic: $generic_number),*);
};
(@impl $inner:ty = $($generic:ident: $generic_number:expr),*) => {
unsafe impl<$($generic: SerializeConst),*> SerializeConst for ($($generic,)*) {
const MEMORY_LAYOUT: Layout = {
Layout::Struct(StructLayout {
size: std::mem::size_of::<($($generic,)*)>(),
data: &[
$(
StructFieldLayout::new(std::mem::offset_of!($inner, $generic_number), $generic::MEMORY_LAYOUT),
)*
],
})
};
}
};
}
impl_serialize_const_tuple!(T1: 0);
impl_serialize_const_tuple!(T1: 0, T2: 1);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8, T10: 9);
const MAX_STR_SIZE: usize = 256;
/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time
#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Hash)]
pub struct ConstStr {
bytes: [u8; MAX_STR_SIZE],
len: u32,
}
#[cfg(feature = "serde")]
mod serde_bytes {
use serde::{Deserialize, Serialize, Serializer};
use crate::ConstStr;
impl Serialize for ConstStr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ConstStr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(ConstStr::new(&s))
}
}
}
unsafe impl SerializeConst for ConstStr {
const MEMORY_LAYOUT: Layout = Layout::Struct(StructLayout {
size: std::mem::size_of::<Self>(),
data: &[
StructFieldLayout::new(
std::mem::offset_of!(Self, bytes),
Layout::List(ListLayout {
len: MAX_STR_SIZE,
item_layout: &Layout::Primitive(PrimitiveLayout {
size: std::mem::size_of::<u8>(),
}),
}),
),
StructFieldLayout::new(
std::mem::offset_of!(Self, len),
Layout::Primitive(PrimitiveLayout {
size: std::mem::size_of::<u32>(),
}),
),
],
});
}
impl ConstStr {
/// Create a new constant string
pub const fn new(s: &str) -> Self {
let str_bytes = s.as_bytes();
let mut bytes = [0; MAX_STR_SIZE];
let mut i = 0;
while i < str_bytes.len() {
bytes[i] = str_bytes[i];
i += 1;
}
Self {
bytes,
len: str_bytes.len() as u32,
}
}
/// Get a reference to the string
pub const fn as_str(&self) -> &str {
let str_bytes = self.bytes.split_at(self.len as usize).0;
match std::str::from_utf8(str_bytes) {
Ok(s) => s,
Err(_) => panic!(
"Invalid utf8; ConstStr should only ever be constructed from valid utf8 strings"
),
}
}
/// Get the length of the string
pub const fn len(&self) -> usize {
self.len as usize
}
/// Check if the string is empty
pub const fn is_empty(&self) -> bool {
self.len == 0
}
/// Push a character onto the string
pub const fn push(self, byte: char) -> Self {
assert!(byte.is_ascii(), "Only ASCII bytes are supported");
let (bytes, len) = char_to_bytes(byte);
let (str, _) = bytes.split_at(len);
let Ok(str) = std::str::from_utf8(str) else {
panic!("Invalid utf8; char_to_bytes should always return valid utf8 bytes")
};
self.push_str(str)
}
/// Push a str onto the string
pub const fn push_str(self, str: &str) -> Self {
let Self { mut bytes, len } = self;
assert!(
str.len() + len as usize <= MAX_STR_SIZE,
"String is too long"
);
let str_bytes = str.as_bytes();
let new_len = len as usize + str_bytes.len();
let mut i = 0;
while i < str_bytes.len() {
bytes[len as usize + i] = str_bytes[i];
i += 1;
}
Self {
bytes,
len: new_len as u32,
}
}
/// Split the string at a byte index. The byte index must be a char boundary
pub const fn split_at(self, index: usize) -> (Self, Self) {
let (left, right) = self.bytes.split_at(index);
let left = match std::str::from_utf8(left) {
Ok(s) => s,
Err(_) => {
panic!("Invalid utf8; you cannot split at a byte that is not a char boundary")
}
};
let right = match std::str::from_utf8(right) {
Ok(s) => s,
Err(_) => {
panic!("Invalid utf8; you cannot split at a byte that is not a char boundary")
}
};
(Self::new(left), Self::new(right))
}
/// Split the string at the last occurrence of a character
pub const fn rsplit_once(&self, char: char) -> Option<(Self, Self)> {
let str = self.as_str();
let mut index = str.len() - 1;
// First find the bytes we are searching for
let (char_bytes, len) = char_to_bytes(char);
let (char_bytes, _) = char_bytes.split_at(len);
let bytes = str.as_bytes();
// Then walk backwards from the end of the string
loop {
let byte = bytes[index];
// Look for char boundaries in the string and check if the bytes match
if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) {
// Split up the string into three sections: [before_char, in_char, after_char]
let (before_char, after_index) = bytes.split_at(index);
let (in_char, after_char) = after_index.split_at(char_boundary_len as usize);
if in_char.len() != char_boundary_len as usize {
panic!("in_char.len() should always be equal to char_boundary_len as usize")
}
// Check if the bytes for the current char and the target char match
let mut in_char_eq = true;
let mut i = 0;
let min_len = if in_char.len() < char_bytes.len() {
in_char.len()
} else {
char_bytes.len()
};
while i < min_len {
in_char_eq &= in_char[i] == char_bytes[i];
i += 1;
}
// If they do, convert the bytes to strings and return the split strings
if in_char_eq {
let Ok(before_char_str) = std::str::from_utf8(before_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
let Ok(after_char_str) = std::str::from_utf8(after_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
return Some((Self::new(before_char_str), Self::new(after_char_str)));
}
}
match index.checked_sub(1) {
Some(new_index) => index = new_index,
None => return None,
}
}
}
/// Split the string at the first occurrence of a character
pub const fn split_once(&self, char: char) -> Option<(Self, Self)> {
let str = self.as_str();
let mut index = 0;
// First find the bytes we are searching for
let (char_bytes, len) = char_to_bytes(char);
let (char_bytes, _) = char_bytes.split_at(len);
let bytes = str.as_bytes();
// Then walk forwards from the start of the string
while index < bytes.len() {
let byte = bytes[index];
// Look for char boundaries in the string and check if the bytes match
if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) {
// Split up the string into three sections: [before_char, in_char, after_char]
let (before_char, after_index) = bytes.split_at(index);
let (in_char, after_char) = after_index.split_at(char_boundary_len as usize);
if in_char.len() != char_boundary_len as usize {
panic!("in_char.len() should always be equal to char_boundary_len as usize")
}
// Check if the bytes for the current char and the target char match
let mut in_char_eq = true;
let mut i = 0;
let min_len = if in_char.len() < char_bytes.len() {
in_char.len()
} else {
char_bytes.len()
};
while i < min_len {
in_char_eq &= in_char[i] == char_bytes[i];
i += 1;
}
// If they do, convert the bytes to strings and return the split strings
if in_char_eq {
let Ok(before_char_str) = std::str::from_utf8(before_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
let Ok(after_char_str) = std::str::from_utf8(after_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
return Some((Self::new(before_char_str), Self::new(after_char_str)));
}
}
index += 1
}
None
}
}
impl std::fmt::Debug for ConstStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.as_str())
}
}
#[test]
fn test_rsplit_once() {
let str = ConstStr::new("hello world");
assert_eq!(
str.rsplit_once(' '),
Some((ConstStr::new("hello"), ConstStr::new("world")))
);
let unicode_str = ConstStr::new("hi😀hello😀world😀world");
assert_eq!(
unicode_str.rsplit_once('😀'),
Some((ConstStr::new("hi😀hello😀world"), ConstStr::new("world")))
);
assert_eq!(unicode_str.rsplit_once('❌'), None);
for _ in 0..100 {
let random_str: String = (0..rand::random::<u8>() % 50)
.map(|_| rand::random::<char>())
.collect();
let konst = ConstStr::new(&random_str);
let mut seen_chars = std::collections::HashSet::new();
for char in random_str.chars().rev() {
let (char_bytes, len) = char_to_bytes(char);
let char_bytes = &char_bytes[..len];
assert_eq!(char_bytes, char.to_string().as_bytes());
if seen_chars.contains(&char) {
continue;
}
seen_chars.insert(char);
let (correct_left, correct_right) = random_str.rsplit_once(char).unwrap();
let (left, right) = konst.rsplit_once(char).unwrap();
println!("splitting {random_str:?} at {char:?}");
assert_eq!(left.as_str(), correct_left);
assert_eq!(right.as_str(), correct_right);
}
}
}
const CONTINUED_CHAR_MASK: u8 = 0b10000000;
const BYTE_CHAR_BOUNDARIES: [u8; 4] = [0b00000000, 0b11000000, 0b11100000, 0b11110000];
// Const version of https://doc.rust-lang.org/src/core/char/methods.rs.html#1765-1797
const fn char_to_bytes(char: char) -> ([u8; 4], usize) {
let code = char as u32;
let len = char.len_utf8();
let mut bytes = [0; 4];
match len {
1 => {
bytes[0] = code as u8;
}
2 => {
bytes[0] = ((code >> 6) & 0x1F) as u8 | BYTE_CHAR_BOUNDARIES[1];
bytes[1] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
}
3 => {
bytes[0] = ((code >> 12) & 0x0F) as u8 | BYTE_CHAR_BOUNDARIES[2];
bytes[1] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
bytes[2] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
}
4 => {
bytes[0] = ((code >> 18) & 0x07) as u8 | BYTE_CHAR_BOUNDARIES[3];
bytes[1] = ((code >> 12) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
bytes[2] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
bytes[3] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
}
_ => panic!(
"encode_utf8: need more than 4 bytes to encode the unicode character, but the buffer has 4 bytes"
),
};
(bytes, len)
}
#[test]
fn fuzz_char_to_bytes() {
use std::char;
for _ in 0..100 {
let char = rand::random::<char>();
let (bytes, len) = char_to_bytes(char);
let str = std::str::from_utf8(&bytes[..len]).unwrap();
assert_eq!(char.to_string(), str);
}
}
const fn utf8_char_boundary_to_char_len(byte: u8) -> Option<u8> {
match byte {
0b00000000..=0b01111111 => Some(1),
0b11000000..=0b11011111 => Some(2),
0b11100000..=0b11101111 => Some(3),
0b11110000..=0b11111111 => Some(4),
_ => None,
}
}
#[test]
fn fuzz_utf8_byte_to_char_len() {
for _ in 0..100 {
let random_string: String = (0..rand::random::<u8>())
.map(|_| rand::random::<char>())
.collect();
let bytes = random_string.as_bytes();
let chars: std::collections::HashMap<_, _> = random_string.char_indices().collect();
for (i, byte) in bytes.iter().enumerate() {
match utf8_char_boundary_to_char_len(*byte) {
Some(char_len) => {
let char = chars
.get(&i)
.unwrap_or_else(|| panic!("{byte:b} is not a character boundary"));
assert_eq!(char.len_utf8(), char_len as usize);
}
None => {
assert!(!chars.contains_key(&i), "{byte:b} is a character boundary");
}
}
}
}
}
/// Serialize a struct that is stored at the pointer passed in
const fn serialize_const_struct(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &StructLayout,
) -> ConstVec<u8> {
let mut i = 0;
while i < layout.data.len() {
// Serialize the field at the offset pointer in the struct
let StructFieldLayout { offset, layout } = &layout.data[i];
let field = ptr.wrapping_byte_add(*offset as _);
to = serialize_const_ptr(field, to, layout);
i += 1;
}
to
}
/// Serialize an enum that is stored at the pointer passed in
const fn serialize_const_enum(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &EnumLayout,
) -> ConstVec<u8> {
let mut discriminant = 0;
let byte_ptr = ptr as *const u8;
let mut offset = 0;
while offset < layout.discriminant.size {
// If the bytes are reversed, walk backwards from the end of the number when pushing bytes
let byte = if cfg!(target_endian = "big") {
unsafe {
byte_ptr
.wrapping_byte_add((layout.discriminant.size - offset - 1) as _)
.read()
}
} else {
unsafe { byte_ptr.wrapping_byte_add(offset as _).read() }
};
to = to.push(byte);
discriminant |= (byte as u32) << (offset * 8);
offset += 1;
}
let mut i = 0;
while i < layout.variants.len() {
// If the variant is the discriminated one, serialize it
let EnumVariant { tag, data, .. } = &layout.variants[i];
if discriminant == *tag {
let data_ptr = ptr.wrapping_byte_offset(layout.variants_offset as _);
to = serialize_const_struct(data_ptr, to, data);
break;
}
i += 1;
}
to
}
/// Serialize a primitive type that is stored at the pointer passed in
const fn serialize_const_primitive(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &PrimitiveLayout,
) -> ConstVec<u8> {
let ptr = ptr as *const u8;
let mut offset = 0;
while offset < layout.size {
// If the bytes are reversed, walk backwards from the end of the number when pushing bytes
if cfg!(any(target_endian = "big", feature = "test-big-endian")) {
to = to.push(unsafe {
ptr.wrapping_byte_offset((layout.size - offset - 1) as _)
.read()
});
} else {
to = to.push(unsafe { ptr.wrapping_byte_offset(offset as _).read() });
}
offset += 1;
}
to
}
/// Serialize a constant sized array that is stored at the pointer passed in
const fn serialize_const_list(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &ListLayout,
) -> ConstVec<u8> {
let len = layout.len;
let mut i = 0;
while i < len {
let field = ptr.wrapping_byte_offset((i * layout.item_layout.size()) as _);
to = serialize_const_ptr(field, to, layout.item_layout);
i += 1;
}
to
}
/// Serialize a pointer to a type that is stored at the pointer passed in
const fn serialize_const_ptr(ptr: *const (), to: ConstVec<u8>, layout: &Layout) -> ConstVec<u8> {
const unsafe fn serialize_const_ptr(
ptr: *const (),
to: ConstVec<u8>,
layout: &Layout,
) -> ConstVec<u8> {
match layout {
Layout::Enum(layout) => serialize_const_enum(ptr, to, layout),
Layout::Struct(layout) => serialize_const_struct(ptr, to, layout),
Layout::Array(layout) => serialize_const_array(ptr, to, layout),
Layout::List(layout) => serialize_const_list(ptr, to, layout),
Layout::Primitive(layout) => serialize_const_primitive(ptr, to, layout),
}
@@ -710,156 +102,31 @@ const fn serialize_const_ptr(ptr: *const (), to: ConstVec<u8>, layout: &Layout)
/// b: 0x22,
/// c: 0x33333333,
/// }, buffer);
/// let buf = buffer.read();
/// assert_eq!(buf.as_ref(), &[0x11, 0x11, 0x11, 0x11, 0x22, 0x33, 0x33, 0x33, 0x33]);
/// assert_eq!(buffer.as_ref(), &[0xa3, 0x61, 0x61, 0x1a, 0x11, 0x11, 0x11, 0x11, 0x61, 0x62, 0x18, 0x22, 0x61, 0x63, 0x1a, 0x33, 0x33, 0x33, 0x33]);
/// ```
#[must_use = "The data is serialized into the returned buffer"]
pub const fn serialize_const<T: SerializeConst>(data: &T, to: ConstVec<u8>) -> ConstVec<u8> {
let ptr = data as *const T as *const ();
serialize_const_ptr(ptr, to, &T::MEMORY_LAYOUT)
}
/// Deserialize a primitive type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
const fn deserialize_const_primitive<'a, const N: usize>(
mut from: ConstReadBuffer<'a>,
layout: &PrimitiveLayout,
out: (usize, [MaybeUninit<u8>; N]),
) -> Option<(ConstReadBuffer<'a>, [MaybeUninit<u8>; N])> {
let (start, mut out) = out;
let mut offset = 0;
while offset < layout.size {
// If the bytes are reversed, walk backwards from the end of the number when filling in bytes
let (from_new, value) = match from.get() {
Some(data) => data,
None => return None,
};
from = from_new;
if cfg!(any(target_endian = "big", feature = "test-big-endian")) {
out[start + layout.size - offset - 1] = MaybeUninit::new(value);
} else {
out[start + offset] = MaybeUninit::new(value);
}
offset += 1;
}
Some((from, out))
}
/// Deserialize a struct type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
const fn deserialize_const_struct<'a, const N: usize>(
mut from: ConstReadBuffer<'a>,
layout: &StructLayout,
out: (usize, [MaybeUninit<u8>; N]),
) -> Option<(ConstReadBuffer<'a>, [MaybeUninit<u8>; N])> {
let (start, mut out) = out;
let mut i = 0;
while i < layout.data.len() {
// Deserialize the field at the offset pointer in the struct
let StructFieldLayout { offset, layout } = &layout.data[i];
let (new_from, new_out) = match deserialize_const_ptr(from, layout, (start + *offset, out))
{
Some(data) => data,
None => return None,
};
from = new_from;
out = new_out;
i += 1;
}
Some((from, out))
}
/// Deserialize an enum type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
const fn deserialize_const_enum<'a, const N: usize>(
mut from: ConstReadBuffer<'a>,
layout: &EnumLayout,
out: (usize, [MaybeUninit<u8>; N]),
) -> Option<(ConstReadBuffer<'a>, [MaybeUninit<u8>; N])> {
let (start, mut out) = out;
let mut discriminant = 0;
// First, deserialize the discriminant
let mut offset = 0;
while offset < layout.discriminant.size {
// If the bytes are reversed, walk backwards from the end of the number when filling in bytes
let (from_new, value) = match from.get() {
Some(data) => data,
None => return None,
};
from = from_new;
if cfg!(target_endian = "big") {
out[start + layout.size - offset - 1] = MaybeUninit::new(value);
discriminant |= (value as u32) << ((layout.discriminant.size - offset - 1) * 8);
} else {
out[start + offset] = MaybeUninit::new(value);
discriminant |= (value as u32) << (offset * 8);
}
offset += 1;
}
// Then, deserialize the variant
let mut i = 0;
let mut matched_variant = false;
while i < layout.variants.len() {
// If the variant is the discriminated one, deserialize it
let EnumVariant { tag, data, .. } = &layout.variants[i];
if discriminant == *tag {
let offset = layout.variants_offset;
let (new_from, new_out) =
match deserialize_const_struct(from, data, (start + offset, out)) {
Some(data) => data,
None => return None,
};
from = new_from;
out = new_out;
matched_variant = true;
break;
}
i += 1;
}
if !matched_variant {
return None;
}
Some((from, out))
}
/// Deserialize a list type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
const fn deserialize_const_list<'a, const N: usize>(
mut from: ConstReadBuffer<'a>,
layout: &ListLayout,
out: (usize, [MaybeUninit<u8>; N]),
) -> Option<(ConstReadBuffer<'a>, [MaybeUninit<u8>; N])> {
let (start, mut out) = out;
let len = layout.len;
let item_layout = layout.item_layout;
let mut i = 0;
while i < len {
let (new_from, new_out) =
match deserialize_const_ptr(from, item_layout, (start + i * item_layout.size(), out)) {
Some(data) => data,
None => return None,
};
from = new_from;
out = new_out;
i += 1;
}
Some((from, out))
// SAFETY: The pointer is valid and the layout is correct
unsafe { serialize_const_ptr(ptr, to, &T::MEMORY_LAYOUT) }
}
/// Deserialize a type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
const fn deserialize_const_ptr<'a, const N: usize>(
from: ConstReadBuffer<'a>,
const fn deserialize_const_ptr<'a>(
from: &'a [u8],
layout: &Layout,
out: (usize, [MaybeUninit<u8>; N]),
) -> Option<(ConstReadBuffer<'a>, [MaybeUninit<u8>; N])> {
out: &mut [MaybeUninit<u8>],
) -> Option<&'a [u8]> {
match layout {
Layout::Enum(layout) => deserialize_const_enum(from, layout, out),
Layout::Struct(layout) => deserialize_const_struct(from, layout, out),
Layout::Array(layout) => deserialize_const_array(from, layout, out),
Layout::List(layout) => deserialize_const_list(from, layout, out),
Layout::Primitive(layout) => deserialize_const_primitive(from, layout, out),
}
}
/// Deserialize a type into the output buffer. Accepts `(type, ConstVec<u8>)` as input and returns `Option<(ConstReadBuffer, Instance of type)>`
/// Deserialize a type into the output buffer. Accepts `(type, ConstVec<u8>)` as input and returns `Option<(&'a [u8], Instance of type)>`
///
/// # Example
/// ```rust
@@ -879,7 +146,7 @@ const fn deserialize_const_ptr<'a, const N: usize>(
/// c: 0x33333333,
/// d: 0x44444444,
/// }, buffer);
/// let buf = buffer.read();
/// let buf = buffer.as_ref();
/// assert_eq!(deserialize_const!(Struct, buf).unwrap().1, Struct {
/// a: 0x11111111,
/// b: 0x22,
@@ -902,14 +169,13 @@ macro_rules! deserialize_const {
/// N must be `std::mem::size_of::<T>()`
#[must_use = "The data is deserialized from the input buffer"]
pub const unsafe fn deserialize_const_raw<const N: usize, T: SerializeConst>(
from: ConstReadBuffer,
) -> Option<(ConstReadBuffer, T)> {
from: &[u8],
) -> Option<(&[u8], T)> {
// Create uninitized memory with the size of the type
let out = [MaybeUninit::uninit(); N];
let mut out = [MaybeUninit::uninit(); N];
// Fill in the bytes into the buffer for the type
let (from, out) = match deserialize_const_ptr(from, &T::MEMORY_LAYOUT, (0, out)) {
Some(data) => data,
None => return None,
let Some(from) = deserialize_const_ptr(from, &T::MEMORY_LAYOUT, &mut out) else {
return None;
};
// Now that the memory is filled in, transmute it into the type
Some((from, unsafe {
+119
View File
@@ -0,0 +1,119 @@
use crate::*;
/// The layout for a dynamically sized list. The list layout is just a length and an item layout.
#[derive(Debug, Copy, Clone)]
pub struct ListLayout {
/// The size of the struct backing the list
pub(crate) size: usize,
/// The byte offset of the length field
len_offset: usize,
/// The layout of the length field
len_layout: PrimitiveLayout,
/// The byte offset of the data field
data_offset: usize,
/// The layout of the data field
data_layout: ArrayLayout,
}
impl ListLayout {
/// Create a new list layout
pub const fn new(
size: usize,
len_offset: usize,
len_layout: PrimitiveLayout,
data_offset: usize,
data_layout: ArrayLayout,
) -> Self {
Self {
size,
len_offset,
len_layout,
data_offset,
data_layout,
}
}
}
/// Serialize a dynamically sized list that is stored at the pointer passed in
pub(crate) const unsafe fn serialize_const_list(
ptr: *const (),
mut to: ConstVec<u8>,
layout: &ListLayout,
) -> ConstVec<u8> {
// Read the length of the list
let len_ptr = ptr.wrapping_byte_offset(layout.len_offset as _);
let len = layout.len_layout.read(len_ptr as *const u8) as usize;
let data_ptr = ptr.wrapping_byte_offset(layout.data_offset as _);
let item_layout = layout.data_layout.item_layout;
// If the item size is 1, deserialize as bytes directly
if item_layout.size() == 1 {
let slice = std::slice::from_raw_parts(data_ptr as *const u8, len);
to = write_bytes(to, slice);
}
// Otherwise, deserialize as a list of items
else {
let mut i = 0;
to = write_array(to, len);
while i < len {
let item = data_ptr.wrapping_byte_offset((i * item_layout.size()) as _);
to = serialize_const_ptr(item, to, item_layout);
i += 1;
}
}
to
}
/// Deserialize a list type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
pub(crate) const fn deserialize_const_list<'a>(
from: &'a [u8],
layout: &ListLayout,
out: &mut [MaybeUninit<u8>],
) -> Option<&'a [u8]> {
let Some((_, len_out)) = out.split_at_mut_checked(layout.len_offset) else {
return None;
};
// If the list items are only one byte, serialize as bytes directly
let item_layout = layout.data_layout.item_layout;
if item_layout.size() == 1 {
let Ok((bytes, new_from)) = take_bytes(from) else {
return None;
};
// Write out the length of the list
layout.len_layout.write(bytes.len() as u32, len_out);
let Some((_, data_out)) = out.split_at_mut_checked(layout.data_offset) else {
return None;
};
let mut offset = 0;
while offset < bytes.len() {
data_out[offset] = MaybeUninit::new(bytes[offset]);
offset += 1;
}
Some(new_from)
}
// Otherwise, serialize as an list of objects
else {
let Ok((len, mut from)) = take_array(from) else {
return None;
};
// Write out the length of the list
layout.len_layout.write(len as u32, len_out);
let Some((_, mut data_out)) = out.split_at_mut_checked(layout.data_offset) else {
return None;
};
let mut i = 0;
while i < len {
let Some(new_from) = deserialize_const_ptr(from, item_layout, data_out) else {
return None;
};
let Some((_, item_out)) = data_out.split_at_mut_checked(item_layout.size()) else {
return None;
};
data_out = item_out;
from = new_from;
i += 1;
}
Some(from)
}
}
+121
View File
@@ -0,0 +1,121 @@
use crate::*;
use std::mem::MaybeUninit;
/// The layout for a primitive type. The bytes will be reversed if the target is big endian.
#[derive(Debug, Copy, Clone)]
pub struct PrimitiveLayout {
pub(crate) size: usize,
}
impl PrimitiveLayout {
/// Create a new primitive layout
pub const fn new(size: usize) -> Self {
Self { size }
}
/// Read the value from the given pointer
///
/// # Safety
/// The pointer must be valid for reads of `self.size` bytes.
pub const unsafe fn read(self, byte_ptr: *const u8) -> u32 {
let mut value = 0;
let mut offset = 0;
while offset < self.size {
// If the bytes are reversed, walk backwards from the end of the number when pushing bytes
let byte = if cfg!(target_endian = "big") {
unsafe {
byte_ptr
.wrapping_byte_add((self.size - offset - 1) as _)
.read()
}
} else {
unsafe { byte_ptr.wrapping_byte_add(offset as _).read() }
};
value |= (byte as u32) << (offset * 8);
offset += 1;
}
value
}
/// Write the value to the given buffer
pub const fn write(self, value: u32, out: &mut [MaybeUninit<u8>]) {
let bytes = value.to_ne_bytes();
let mut offset = 0;
while offset < self.size {
out[offset] = MaybeUninit::new(bytes[offset]);
offset += 1;
}
}
}
macro_rules! impl_serialize_const {
($type:ty) => {
unsafe impl SerializeConst for $type {
const MEMORY_LAYOUT: Layout = Layout::Primitive(PrimitiveLayout {
size: std::mem::size_of::<$type>(),
});
}
};
}
impl_serialize_const!(u8);
impl_serialize_const!(u16);
impl_serialize_const!(u32);
impl_serialize_const!(u64);
impl_serialize_const!(i8);
impl_serialize_const!(i16);
impl_serialize_const!(i32);
impl_serialize_const!(i64);
impl_serialize_const!(bool);
impl_serialize_const!(f32);
impl_serialize_const!(f64);
/// Serialize a primitive type that is stored at the pointer passed in
pub(crate) const unsafe fn serialize_const_primitive(
ptr: *const (),
to: ConstVec<u8>,
layout: &PrimitiveLayout,
) -> ConstVec<u8> {
let ptr = ptr as *const u8;
let mut offset = 0;
let mut i64_bytes = [0u8; 8];
while offset < layout.size {
// If the bytes are reversed, walk backwards from the end of the number when pushing bytes
let byte = unsafe {
if cfg!(any(target_endian = "big", feature = "test-big-endian")) {
ptr.wrapping_byte_offset((layout.size - offset - 1) as _)
.read()
} else {
ptr.wrapping_byte_offset(offset as _).read()
}
};
i64_bytes[offset] = byte;
offset += 1;
}
let number = i64::from_ne_bytes(i64_bytes);
write_number(to, number)
}
/// Deserialize a primitive type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
pub(crate) const fn deserialize_const_primitive<'a>(
from: &'a [u8],
layout: &PrimitiveLayout,
out: &mut [MaybeUninit<u8>],
) -> Option<&'a [u8]> {
let mut offset = 0;
let Ok((number, from)) = take_number(from) else {
return None;
};
let bytes = number.to_le_bytes();
while offset < layout.size {
// If the bytes are reversed, walk backwards from the end of the number when filling in bytes
let byte = bytes[offset];
if cfg!(any(target_endian = "big", feature = "test-big-endian")) {
out[layout.size - offset - 1] = MaybeUninit::new(byte);
} else {
out[offset] = MaybeUninit::new(byte);
}
offset += 1;
}
Some(from)
}
+429
View File
@@ -0,0 +1,429 @@
use crate::*;
use std::{char, fmt::Debug, hash::Hash, mem::MaybeUninit};
const MAX_STR_SIZE: usize = 256;
/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time
#[derive(Clone, Copy)]
pub struct ConstStr {
bytes: [MaybeUninit<u8>; MAX_STR_SIZE],
len: u32,
}
impl Debug for ConstStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConstStr")
.field("str", &self.as_str())
.finish()
}
}
#[cfg(feature = "serde")]
mod serde_bytes {
use serde::{Deserialize, Serialize, Serializer};
use crate::ConstStr;
impl Serialize for ConstStr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ConstStr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(ConstStr::new(&s))
}
}
}
unsafe impl SerializeConst for ConstStr {
const MEMORY_LAYOUT: Layout = Layout::List(ListLayout::new(
std::mem::size_of::<Self>(),
std::mem::offset_of!(Self, len),
PrimitiveLayout {
size: std::mem::size_of::<u32>(),
},
std::mem::offset_of!(Self, bytes),
ArrayLayout {
len: MAX_STR_SIZE,
item_layout: &Layout::Primitive(PrimitiveLayout {
size: std::mem::size_of::<u8>(),
}),
},
));
}
#[cfg(feature = "const-serialize-07")]
unsafe impl const_serialize_07::SerializeConst for ConstStr {
const MEMORY_LAYOUT: const_serialize_07::Layout =
const_serialize_07::Layout::Struct(const_serialize_07::StructLayout::new(
std::mem::size_of::<Self>(),
&[
const_serialize_07::StructFieldLayout::new(
std::mem::offset_of!(Self, bytes),
const_serialize_07::Layout::List(const_serialize_07::ListLayout::new(
MAX_STR_SIZE,
&const_serialize_07::Layout::Primitive(
const_serialize_07::PrimitiveLayout::new(std::mem::size_of::<u8>()),
),
)),
),
const_serialize_07::StructFieldLayout::new(
std::mem::offset_of!(Self, len),
const_serialize_07::Layout::Primitive(
const_serialize_07::PrimitiveLayout::new(std::mem::size_of::<u32>()),
),
),
],
));
}
impl ConstStr {
/// Create a new constant string
pub const fn new(s: &str) -> Self {
let str_bytes = s.as_bytes();
// This is serialized as a constant sized array in const-serialize-07 which requires all memory to be initialized
let mut bytes = if cfg!(feature = "const-serialize-07") {
[MaybeUninit::new(0); MAX_STR_SIZE]
} else {
[MaybeUninit::uninit(); MAX_STR_SIZE]
};
let mut i = 0;
while i < str_bytes.len() {
bytes[i] = MaybeUninit::new(str_bytes[i]);
i += 1;
}
Self {
bytes,
len: str_bytes.len() as u32,
}
}
/// Get the bytes of the initialized portion of the string
const fn bytes(&self) -> &[u8] {
// Safety: All bytes up to the pointer are initialized
unsafe {
&*(self.bytes.split_at(self.len as usize).0 as *const [MaybeUninit<u8>]
as *const [u8])
}
}
/// Get a reference to the string
pub const fn as_str(&self) -> &str {
let str_bytes = self.bytes();
match std::str::from_utf8(str_bytes) {
Ok(s) => s,
Err(_) => panic!(
"Invalid utf8; ConstStr should only ever be constructed from valid utf8 strings"
),
}
}
/// Get the length of the string
pub const fn len(&self) -> usize {
self.len as usize
}
/// Check if the string is empty
pub const fn is_empty(&self) -> bool {
self.len == 0
}
/// Push a character onto the string
pub const fn push(self, byte: char) -> Self {
assert!(byte.is_ascii(), "Only ASCII bytes are supported");
let (bytes, len) = char_to_bytes(byte);
let (str, _) = bytes.split_at(len);
let Ok(str) = std::str::from_utf8(str) else {
panic!("Invalid utf8; char_to_bytes should always return valid utf8 bytes")
};
self.push_str(str)
}
/// Push a str onto the string
pub const fn push_str(self, str: &str) -> Self {
let Self { mut bytes, len } = self;
assert!(
str.len() + len as usize <= MAX_STR_SIZE,
"String is too long"
);
let str_bytes = str.as_bytes();
let new_len = len as usize + str_bytes.len();
let mut i = 0;
while i < str_bytes.len() {
bytes[len as usize + i] = MaybeUninit::new(str_bytes[i]);
i += 1;
}
Self {
bytes,
len: new_len as u32,
}
}
/// Split the string at a byte index. The byte index must be a char boundary
pub const fn split_at(self, index: usize) -> (Self, Self) {
let (left, right) = self.bytes().split_at(index);
let left = match std::str::from_utf8(left) {
Ok(s) => s,
Err(_) => {
panic!("Invalid utf8; you cannot split at a byte that is not a char boundary")
}
};
let right = match std::str::from_utf8(right) {
Ok(s) => s,
Err(_) => {
panic!("Invalid utf8; you cannot split at a byte that is not a char boundary")
}
};
(Self::new(left), Self::new(right))
}
/// Split the string at the last occurrence of a character
pub const fn rsplit_once(&self, char: char) -> Option<(Self, Self)> {
let str = self.as_str();
let mut index = str.len() - 1;
// First find the bytes we are searching for
let (char_bytes, len) = char_to_bytes(char);
let (char_bytes, _) = char_bytes.split_at(len);
let bytes = str.as_bytes();
// Then walk backwards from the end of the string
loop {
let byte = bytes[index];
// Look for char boundaries in the string and check if the bytes match
if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) {
// Split up the string into three sections: [before_char, in_char, after_char]
let (before_char, after_index) = bytes.split_at(index);
let (in_char, after_char) = after_index.split_at(char_boundary_len as usize);
if in_char.len() != char_boundary_len as usize {
panic!("in_char.len() should always be equal to char_boundary_len as usize")
}
// Check if the bytes for the current char and the target char match
let mut in_char_eq = true;
let mut i = 0;
let min_len = if in_char.len() < char_bytes.len() {
in_char.len()
} else {
char_bytes.len()
};
while i < min_len {
in_char_eq &= in_char[i] == char_bytes[i];
i += 1;
}
// If they do, convert the bytes to strings and return the split strings
if in_char_eq {
let Ok(before_char_str) = std::str::from_utf8(before_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
let Ok(after_char_str) = std::str::from_utf8(after_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
return Some((Self::new(before_char_str), Self::new(after_char_str)));
}
}
match index.checked_sub(1) {
Some(new_index) => index = new_index,
None => return None,
}
}
}
/// Split the string at the first occurrence of a character
pub const fn split_once(&self, char: char) -> Option<(Self, Self)> {
let str = self.as_str();
let mut index = 0;
// First find the bytes we are searching for
let (char_bytes, len) = char_to_bytes(char);
let (char_bytes, _) = char_bytes.split_at(len);
let bytes = str.as_bytes();
// Then walk forwards from the start of the string
while index < bytes.len() {
let byte = bytes[index];
// Look for char boundaries in the string and check if the bytes match
if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) {
// Split up the string into three sections: [before_char, in_char, after_char]
let (before_char, after_index) = bytes.split_at(index);
let (in_char, after_char) = after_index.split_at(char_boundary_len as usize);
if in_char.len() != char_boundary_len as usize {
panic!("in_char.len() should always be equal to char_boundary_len as usize")
}
// Check if the bytes for the current char and the target char match
let mut in_char_eq = true;
let mut i = 0;
let min_len = if in_char.len() < char_bytes.len() {
in_char.len()
} else {
char_bytes.len()
};
while i < min_len {
in_char_eq &= in_char[i] == char_bytes[i];
i += 1;
}
// If they do, convert the bytes to strings and return the split strings
if in_char_eq {
let Ok(before_char_str) = std::str::from_utf8(before_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
let Ok(after_char_str) = std::str::from_utf8(after_char) else {
panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary")
};
return Some((Self::new(before_char_str), Self::new(after_char_str)));
}
}
index += 1
}
None
}
}
impl PartialEq for ConstStr {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Eq for ConstStr {}
impl PartialOrd for ConstStr {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ConstStr {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_str().cmp(other.as_str())
}
}
impl Hash for ConstStr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
#[test]
fn test_rsplit_once() {
let str = ConstStr::new("hello world");
assert_eq!(
str.rsplit_once(' '),
Some((ConstStr::new("hello"), ConstStr::new("world")))
);
let unicode_str = ConstStr::new("hi😀hello😀world😀world");
assert_eq!(
unicode_str.rsplit_once('😀'),
Some((ConstStr::new("hi😀hello😀world"), ConstStr::new("world")))
);
assert_eq!(unicode_str.rsplit_once('❌'), None);
for _ in 0..100 {
let random_str: String = (0..rand::random::<u8>() % 50)
.map(|_| rand::random::<char>())
.collect();
let konst = ConstStr::new(&random_str);
let mut seen_chars = std::collections::HashSet::new();
for char in random_str.chars().rev() {
let (char_bytes, len) = char_to_bytes(char);
let char_bytes = &char_bytes[..len];
assert_eq!(char_bytes, char.to_string().as_bytes());
if seen_chars.contains(&char) {
continue;
}
seen_chars.insert(char);
let (correct_left, correct_right) = random_str.rsplit_once(char).unwrap();
let (left, right) = konst.rsplit_once(char).unwrap();
println!("splitting {random_str:?} at {char:?}");
assert_eq!(left.as_str(), correct_left);
assert_eq!(right.as_str(), correct_right);
}
}
}
const CONTINUED_CHAR_MASK: u8 = 0b10000000;
const BYTE_CHAR_BOUNDARIES: [u8; 4] = [0b00000000, 0b11000000, 0b11100000, 0b11110000];
// Const version of https://doc.rust-lang.org/src/core/char/methods.rs.html#1765-1797
const fn char_to_bytes(char: char) -> ([u8; 4], usize) {
let code = char as u32;
let len = char.len_utf8();
let mut bytes = [0; 4];
match len {
1 => {
bytes[0] = code as u8;
}
2 => {
bytes[0] = ((code >> 6) & 0x1F) as u8 | BYTE_CHAR_BOUNDARIES[1];
bytes[1] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
}
3 => {
bytes[0] = ((code >> 12) & 0x0F) as u8 | BYTE_CHAR_BOUNDARIES[2];
bytes[1] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
bytes[2] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
}
4 => {
bytes[0] = ((code >> 18) & 0x07) as u8 | BYTE_CHAR_BOUNDARIES[3];
bytes[1] = ((code >> 12) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
bytes[2] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
bytes[3] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
}
_ => panic!(
"encode_utf8: need more than 4 bytes to encode the unicode character, but the buffer has 4 bytes"
),
};
(bytes, len)
}
#[test]
fn fuzz_char_to_bytes() {
use std::char;
for _ in 0..100 {
let char = rand::random::<char>();
let (bytes, len) = char_to_bytes(char);
let str = std::str::from_utf8(&bytes[..len]).unwrap();
assert_eq!(char.to_string(), str);
}
}
const fn utf8_char_boundary_to_char_len(byte: u8) -> Option<u8> {
match byte {
0b00000000..=0b01111111 => Some(1),
0b11000000..=0b11011111 => Some(2),
0b11100000..=0b11101111 => Some(3),
0b11110000..=0b11111111 => Some(4),
_ => None,
}
}
#[test]
fn fuzz_utf8_byte_to_char_len() {
for _ in 0..100 {
let random_string: String = (0..rand::random::<u8>())
.map(|_| rand::random::<char>())
.collect();
let bytes = random_string.as_bytes();
let chars: std::collections::HashMap<_, _> = random_string.char_indices().collect();
for (i, byte) in bytes.iter().enumerate() {
match utf8_char_boundary_to_char_len(*byte) {
Some(char_len) => {
let char = chars
.get(&i)
.unwrap_or_else(|| panic!("{byte:b} is not a character boundary"));
assert_eq!(char.len_utf8(), char_len as usize);
}
None => {
assert!(!chars.contains_key(&i), "{byte:b} is a character boundary");
}
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
use crate::*;
/// Plain old data for a field. Stores the offset of the field in the struct and the layout of the field.
#[derive(Debug, Copy, Clone)]
pub struct StructFieldLayout {
name: &'static str,
offset: usize,
layout: Layout,
}
impl StructFieldLayout {
/// Create a new struct field layout
pub const fn new(name: &'static str, offset: usize, layout: Layout) -> Self {
Self {
name,
offset,
layout,
}
}
}
/// Layout for a struct. The struct layout is just a list of fields with offsets
#[derive(Debug, Copy, Clone)]
pub struct StructLayout {
pub(crate) size: usize,
pub(crate) data: &'static [StructFieldLayout],
}
impl StructLayout {
/// Create a new struct layout
pub const fn new(size: usize, data: &'static [StructFieldLayout]) -> Self {
Self { size, data }
}
}
/// Serialize a struct that is stored at the pointer passed in
pub(crate) const unsafe fn serialize_const_struct(
ptr: *const (),
to: ConstVec<u8>,
layout: &StructLayout,
) -> ConstVec<u8> {
let mut i = 0;
let field_count = layout.data.len();
let mut to = write_map(to, field_count);
while i < field_count {
// Serialize the field at the offset pointer in the struct
let StructFieldLayout {
name,
offset,
layout,
} = &layout.data[i];
to = write_map_key(to, name);
let field = ptr.wrapping_byte_add(*offset as _);
to = serialize_const_ptr(field, to, layout);
i += 1;
}
to
}
/// Deserialize a struct type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added.
pub(crate) const fn deserialize_const_struct<'a>(
from: &'a [u8],
layout: &StructLayout,
out: &mut [MaybeUninit<u8>],
) -> Option<&'a [u8]> {
let Ok((map, from)) = take_map(from) else {
return None;
};
let mut i = 0;
while i < layout.data.len() {
// Deserialize the field at the offset pointer in the struct
let StructFieldLayout {
name,
offset,
layout,
} = &layout.data[i];
let Ok(Some(from)) = map.find(name) else {
return None;
};
let Some((_, field_bytes)) = out.split_at_mut_checked(*offset) else {
return None;
};
if deserialize_const_ptr(from, layout, field_bytes).is_none() {
return None;
}
i += 1;
}
Some(from)
}
macro_rules! impl_serialize_const_tuple {
($($generic:ident: $generic_number:expr),*) => {
impl_serialize_const_tuple!(@impl ($($generic,)*) = $($generic: $generic_number),*);
};
(@impl $inner:ty = $($generic:ident: $generic_number:expr),*) => {
unsafe impl<$($generic: SerializeConst),*> SerializeConst for ($($generic,)*) {
const MEMORY_LAYOUT: Layout = {
Layout::Struct(StructLayout {
size: std::mem::size_of::<($($generic,)*)>(),
data: &[
$(
StructFieldLayout::new(stringify!($generic_number), std::mem::offset_of!($inner, $generic_number), $generic::MEMORY_LAYOUT),
)*
],
})
};
}
};
}
impl_serialize_const_tuple!(T1: 0);
impl_serialize_const_tuple!(T1: 0, T2: 1);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8);
impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8, T10: 9);
+88 -13
View File
@@ -81,7 +81,7 @@ fn test_serialize_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
let data = Enum::B {
@@ -91,7 +91,7 @@ fn test_serialize_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
}
@@ -110,7 +110,7 @@ fn test_serialize_list_of_lopsided_enums() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
let data = [
@@ -126,7 +126,7 @@ fn test_serialize_list_of_lopsided_enums() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
let data = [
@@ -139,7 +139,7 @@ fn test_serialize_list_of_lopsided_enums() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
let data = [
@@ -152,7 +152,7 @@ fn test_serialize_list_of_lopsided_enums() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
}
@@ -171,14 +171,14 @@ fn test_serialize_u8_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
let data = Enum::B;
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
}
@@ -198,7 +198,7 @@ fn test_serialize_corrupted_enum() {
buf = serialize_const(&data, buf);
buf = buf.set(0, 2);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf), None);
}
@@ -226,7 +226,7 @@ fn test_serialize_nested_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
let data = Enum::B {
@@ -236,7 +236,7 @@ fn test_serialize_nested_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
let data = Enum::B {
@@ -249,7 +249,7 @@ fn test_serialize_nested_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
let data = Enum::B {
@@ -262,6 +262,81 @@ fn test_serialize_nested_enum() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
}
#[test]
fn test_adding_enum_field_non_breaking() {
#[derive(Debug, PartialEq, SerializeConst)]
#[repr(C, u8)]
enum Initial {
A { a: u32, b: u8 },
}
#[derive(Debug, PartialEq, SerializeConst)]
#[repr(C, u8)]
enum New {
A { b: u8, a: u32, c: u32 },
}
let data = New::A {
a: 0x11111111,
b: 0x22,
c: 0x33333333,
};
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
let buf = buf.as_ref();
// The new struct should be able to deserialize into the initial struct
let (_, data2) = deserialize_const!(Initial, buf).unwrap();
assert_eq!(
Initial::A {
a: 0x11111111,
b: 0x22,
},
data2
);
}
#[test]
fn test_adding_enum_variant_non_breaking() {
#[derive(Debug, PartialEq, SerializeConst)]
#[repr(C, u8)]
enum Initial {
A { a: u32, b: u8 },
}
#[derive(Debug, PartialEq, SerializeConst)]
#[repr(C, u8)]
enum New {
#[allow(unused)]
B {
d: u32,
e: u8,
},
A {
c: u32,
b: u8,
a: u32,
},
}
let data = New::A {
a: 0x11111111,
b: 0x22,
c: 0x33333333,
};
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
let buf = buf.as_ref();
// The new struct should be able to deserialize into the initial struct
let (_, data2) = deserialize_const!(Initial, buf).unwrap();
assert_eq!(
Initial::A {
a: 0x11111111,
b: 0x22,
},
data2
);
}
+3 -3
View File
@@ -5,7 +5,7 @@ fn test_serialize_const_layout_list() {
let mut buf = ConstVec::new();
buf = serialize_const(&[1u8, 2, 3] as &[u8; 3], buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!([u8; 3], buf).unwrap().1, [1, 2, 3])
}
@@ -17,7 +17,7 @@ fn test_serialize_const_layout_nested_lists() {
buf,
);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(
deserialize_const!([[u8; 3]; 3], buf).unwrap().1,
@@ -29,6 +29,6 @@ fn test_serialize_const_layout_nested_lists() {
fn test_serialize_list_too_little_data() {
let mut buf = ConstVec::new();
buf = buf.push(1);
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!([u64; 10], buf), None);
}
+8 -33
View File
@@ -4,58 +4,33 @@ use const_serialize::{deserialize_const, serialize_const, ConstVec};
fn test_serialize_const_layout_primitive() {
let mut buf = ConstVec::new();
buf = serialize_const(&1234u32, buf);
if cfg!(feature = "test-big-endian") {
assert_eq!(buf.as_ref(), 1234u32.to_be_bytes());
} else {
assert_eq!(buf.as_ref(), 1234u32.to_le_bytes());
}
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(u32, buf).unwrap().1, 1234u32);
let mut buf = ConstVec::new();
buf = serialize_const(&1234u64, buf);
if cfg!(feature = "test-big-endian") {
assert_eq!(buf.as_ref(), 1234u64.to_be_bytes());
} else {
assert_eq!(buf.as_ref(), 1234u64.to_le_bytes());
}
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(u64, buf).unwrap().1, 1234u64);
let mut buf = ConstVec::new();
buf = serialize_const(&1234i32, buf);
if cfg!(feature = "test-big-endian") {
assert_eq!(buf.as_ref(), 1234i32.to_be_bytes());
} else {
assert_eq!(buf.as_ref(), 1234i32.to_le_bytes());
}
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(i32, buf).unwrap().1, 1234i32);
let mut buf = ConstVec::new();
buf = serialize_const(&1234i64, buf);
if cfg!(feature = "test-big-endian") {
assert_eq!(buf.as_ref(), 1234i64.to_be_bytes());
} else {
assert_eq!(buf.as_ref(), 1234i64.to_le_bytes());
}
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(i64, buf).unwrap().1, 1234i64);
let mut buf = ConstVec::new();
buf = serialize_const(&true, buf);
assert_eq!(buf.as_ref(), [1u8]);
let buf = buf.read();
let buf = buf.as_ref();
assert!(deserialize_const!(bool, buf).unwrap().1);
let mut buf = ConstVec::new();
buf = serialize_const(&0.631f32, buf);
if cfg!(feature = "test-big-endian") {
assert_eq!(buf.as_ref(), 0.631f32.to_be_bytes());
} else {
assert_eq!(buf.as_ref(), 0.631f32.to_le_bytes());
}
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(f32, buf).unwrap().1, 0.631);
}
@@ -66,6 +41,6 @@ fn test_serialize_primitive_too_little_data() {
buf = buf.push(1);
buf = buf.push(1);
buf = buf.push(1);
let buf = buf.read();
assert_eq!(deserialize_const!(u64, buf), None);
let buf = buf.as_ref();
assert_eq!(deserialize_const!([u64; 10], buf), None);
}
+7 -7
View File
@@ -6,11 +6,10 @@ fn test_serialize_const_layout_str() {
let str = ConstStr::new("hello");
buf = serialize_const(&str, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
assert_eq!(
deserialize_const!(ConstStr, buf).unwrap().1.as_str(),
"hello"
);
let buf = buf.as_ref();
assert!(buf.len() < 10);
let str = deserialize_const!(ConstStr, buf).unwrap().1;
assert_eq!(str.as_str(), "hello");
}
#[test]
@@ -19,7 +18,8 @@ fn test_serialize_const_layout_nested_str() {
let str = ConstStr::new("hello");
buf = serialize_const(&[str, str, str] as &[ConstStr; 3], buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
assert!(buf.len() < 30);
let buf = buf.as_ref();
assert_eq!(
deserialize_const!([ConstStr; 3], buf).unwrap().1,
@@ -35,6 +35,6 @@ fn test_serialize_const_layout_nested_str() {
fn test_serialize_str_too_little_data() {
let mut buf = ConstVec::new();
buf = buf.push(1);
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(deserialize_const!(ConstStr, buf), None);
}
+38 -4
View File
@@ -96,7 +96,7 @@ fn test_serialize_const_layout_struct_list() {
const _ASSERT: () = {
let mut buf = ConstVec::new();
buf = serialize_const(&DATA, buf);
let buf = buf.read();
let buf = buf.as_ref();
let [first, second, third] = match deserialize_const!([OtherStruct; 3], buf) {
Some((_, data)) => data,
None => panic!("data mismatch"),
@@ -109,7 +109,7 @@ fn test_serialize_const_layout_struct_list() {
let mut buf = ConstVec::new();
const DATA_AGAIN: [[OtherStruct; 3]; 3] = [DATA, DATA, DATA];
buf = serialize_const(&DATA_AGAIN, buf);
let buf = buf.read();
let buf = buf.as_ref();
let [first, second, third] = match deserialize_const!([[OtherStruct; 3]; 3], buf) {
Some((_, data)) => data,
None => panic!("data mismatch"),
@@ -128,7 +128,7 @@ fn test_serialize_const_layout_struct_list() {
let mut buf = ConstVec::new();
buf = serialize_const(&DATA, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
let (_, data2) = deserialize_const!([OtherStruct; 3], buf).unwrap();
assert_eq!(DATA, data2);
}
@@ -158,7 +158,41 @@ fn test_serialize_const_layout_struct() {
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
println!("{:?}", buf.as_ref());
let buf = buf.read();
let buf = buf.as_ref();
let (_, data2) = deserialize_const!(OtherStruct, buf).unwrap();
assert_eq!(data, data2);
}
#[test]
fn test_adding_struct_field_non_breaking() {
#[derive(Debug, PartialEq, SerializeConst)]
struct Initial {
a: u32,
b: u8,
}
#[derive(Debug, PartialEq, SerializeConst)]
struct New {
c: u32,
b: u8,
a: u32,
}
let data = New {
a: 0x11111111,
b: 0x22,
c: 0x33333333,
};
let mut buf = ConstVec::new();
buf = serialize_const(&data, buf);
let buf = buf.as_ref();
// The new struct should be able to deserialize into the initial struct
let (_, data2) = deserialize_const!(Initial, buf).unwrap();
assert_eq!(
Initial {
a: data.a,
b: data.b,
},
data2
);
}
+3 -3
View File
@@ -4,7 +4,7 @@ use const_serialize::{deserialize_const, serialize_const, ConstVec};
fn test_serialize_const_layout_tuple() {
let mut buf = ConstVec::new();
buf = serialize_const(&(1234u32, 5678u16), buf);
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(
deserialize_const!((u32, u16), buf).unwrap().1,
(1234u32, 5678u16)
@@ -12,7 +12,7 @@ fn test_serialize_const_layout_tuple() {
let mut buf = ConstVec::new();
buf = serialize_const(&(1234f64, 5678u16, 90u8), buf);
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(
deserialize_const!((f64, u16, u8), buf).unwrap().1,
(1234f64, 5678u16, 90u8)
@@ -20,7 +20,7 @@ fn test_serialize_const_layout_tuple() {
let mut buf = ConstVec::new();
buf = serialize_const(&(1234u32, 5678u16, 90u8, 1000000f64), buf);
let buf = buf.read();
let buf = buf.as_ref();
assert_eq!(
deserialize_const!((u32, u16, u8, f64), buf).unwrap().1,
(1234u32, 5678u16, 90u8, 1000000f64)
+1 -1
View File
@@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["web", "desktop", "mobile", "gui", "wasm"]
rust-version = "1.80.0"
rust-version = "1.83.0"
[dependencies]
dioxus-core = { workspace = true }
+1 -1
View File
@@ -7,7 +7,7 @@ description = "A box backed by a generational runtime"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
keywords = ["generational", "box", "memory", "allocator"]
rust-version = "1.80.0"
rust-version = "1.83.0"
[dependencies]
parking_lot = { workspace = true }
+2 -2
View File
@@ -14,13 +14,13 @@ keywords = ["dom", "ui", "gui", "assets"]
# prevent the main code from compiling!
[dependencies]
serde = { workspace = true, features = ["derive"] }
const-serialize = { workspace = true, features = ["serde"] }
const-serialize-08 = { workspace = true, features = ["serde", "const-serialize-07"] }
const-serialize-07 = { workspace = true }
dioxus-core-types = { workspace = true, optional = true }
dioxus-cli-config = { workspace = true, optional = true }
[dev-dependencies]
manganis = { workspace = true }
dioxus = { workspace = true }
[features]
dioxus = ["dep:dioxus-core-types", "dep:dioxus-cli-config"]
+46 -10
View File
@@ -1,5 +1,6 @@
use crate::AssetOptions;
use const_serialize::{deserialize_const, ConstStr, ConstVec, SerializeConst};
use const_serialize_07 as const_serialize;
use const_serialize_08::{deserialize_const, ConstStr, 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
@@ -7,7 +8,17 @@ use std::{fmt::Debug, hash::Hash, path::PathBuf};
/// 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, Eq, Clone, Copy, SerializeConst, serde::Serialize, serde::Deserialize)]
#[derive(
Debug,
Eq,
Clone,
Copy,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
pub struct BundledAsset {
/// The absolute path of the asset
absolute_source_path: ConstStr,
@@ -51,6 +62,8 @@ impl Hash for BundledAsset {
}
impl BundledAsset {
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.";
#[doc(hidden)]
/// This should only be called from the macro
/// Create a new asset
@@ -86,7 +99,7 @@ impl BundledAsset {
/// It should not be read directly with [`std::fs::read`] because the path needs to be resolved
/// relative to the bundle
///
/// ```rust
/// ```rust, ignore
/// # use manganis::{asset, Asset};
/// # use dioxus::prelude::*;
/// const ASSET: Asset = asset!("/assets/image.png");
@@ -99,13 +112,15 @@ impl BundledAsset {
pub struct Asset {
/// A function that returns a pointer to the bundled asset. This will be resolved after the linker has run and
/// put into the lazy asset. We use a function instead of using the pointer directly to force the compiler to
/// read the static __REFERENCE_TO_LINK_SECTION at runtime which will be offset by the hot reloading engine instead
/// read the static __LINK_SECTION at runtime which will be offset by the hot reloading engine instead
/// of at compile time which can't be offset
///
/// 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: fn() -> &'static [u8],
/// The legacy version of [`Self::bundled`]. This is only used for backwards compatibility with older versions of the CLI
legacy: fn() -> &'static [u8],
}
impl Debug for Asset {
@@ -121,27 +136,48 @@ 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: extern "Rust" fn() -> &'static [u8]) -> Self {
Self { bundled }
pub const fn new(
bundled: extern "Rust" fn() -> &'static [u8],
legacy: extern "Rust" fn() -> &'static [u8],
) -> Self {
Self { bundled, legacy }
}
/// Get the bundled asset
pub fn bundled(&self) -> BundledAsset {
let bundled = (self.bundled)();
let len = bundled.len();
// Read the slice using volatile reads to prevent the compiler from optimizing
// away the read at compile time
fn read_slice_volatile(bundled: &'static [u8]) -> const_serialize_07::ConstVec<u8> {
let ptr = bundled as *const [u8] as *const u8;
let len = bundled.len();
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();
let mut bytes = const_serialize_07::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
let byte = unsafe { std::ptr::read_volatile(ptr.add(byte)) };
bytes = bytes.push(byte);
}
bytes
}
let bundled = (self.bundled)();
let bytes = read_slice_volatile(bundled);
let read = bytes.as_ref();
let asset = deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
// If the asset wasn't bundled with the newer format, try the legacy format
if asset.bundled_path() == BundledAsset::PLACEHOLDER_HASH {
let bundled = (self.legacy)();
let bytes = read_slice_volatile(bundled);
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
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;
asset
} else {
asset
}
}
/// Return a canonicalized path to the asset
+4 -1
View File
@@ -1,5 +1,6 @@
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst;
use const_serialize_07 as const_serialize;
use const_serialize_08::SerializeConst;
/// Options for a css asset
#[derive(
@@ -11,9 +12,11 @@ use const_serialize::SerializeConst;
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
pub struct CssAssetOptions {
minify: bool,
preload: bool,
@@ -1,5 +1,6 @@
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst;
use const_serialize_07 as const_serialize;
use const_serialize_08::SerializeConst;
use std::collections::HashSet;
/// Options for a css module asset
@@ -12,9 +13,11 @@ use std::collections::HashSet;
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
#[non_exhaustive]
#[doc(hidden)]
pub struct CssModuleAssetOptions {
@@ -1,4 +1,5 @@
use const_serialize::SerializeConst;
use const_serialize_07 as const_serialize;
use const_serialize_08::SerializeConst;
use crate::{AssetOptions, AssetOptionsBuilder};
@@ -12,9 +13,11 @@ use crate::{AssetOptions, AssetOptionsBuilder};
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
pub struct FolderAssetOptions {}
impl Default for FolderAssetOptions {
@@ -1,4 +1,5 @@
use const_serialize::SerializeConst;
use const_serialize_07 as const_serialize;
use const_serialize_08::SerializeConst;
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
@@ -12,9 +13,11 @@ use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
#[repr(u8)]
pub enum ImageFormat {
/// A png image. Png images cannot contain transparency and tend to compress worse than other formats
@@ -39,9 +42,11 @@ pub enum ImageFormat {
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
#[repr(C, u8)]
pub enum ImageSize {
/// A manual size in pixels
@@ -65,9 +70,11 @@ pub enum ImageSize {
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
pub struct ImageAssetOptions {
ty: ImageFormat,
low_quality_preview: bool,
+4 -1
View File
@@ -1,4 +1,5 @@
use const_serialize::SerializeConst;
use const_serialize_07 as const_serialize;
use const_serialize_08::SerializeConst;
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
@@ -12,9 +13,11 @@ use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
pub struct JsAssetOptions {
minify: bool,
preload: bool,
@@ -1,4 +1,5 @@
use const_serialize::SerializeConst;
use const_serialize_07 as const_serialize;
use const_serialize_08::SerializeConst;
use crate::{
CssAssetOptions, CssModuleAssetOptions, FolderAssetOptions, ImageAssetOptions, JsAssetOptions,
@@ -14,9 +15,11 @@ use crate::{
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
#[non_exhaustive]
pub struct AssetOptions {
/// If a hash should be added to the asset path
@@ -107,7 +110,7 @@ impl AssetOptionsBuilder<()> {
impl<T> AssetOptionsBuilder<T> {
/// Create a new asset options builder with the given variant
pub(crate) const fn variant(variant: T) -> Self {
pub const fn variant(variant: T) -> Self {
Self {
add_hash: true,
variant,
@@ -153,9 +156,11 @@ impl<T> AssetOptionsBuilder<T> {
Copy,
Hash,
SerializeConst,
const_serialize::SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[const_serialize(crate = const_serialize_08)]
#[repr(C, u8)]
#[non_exhaustive]
pub enum AssetVariant {
@@ -119,9 +119,10 @@ impl AssetParser {
#link_section
static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION;
manganis::Asset::new(|| unsafe { std::ptr::read_volatile(&__REFERENCE_TO_LINK_SECTION) })
manganis::Asset::new(
|| unsafe { std::ptr::read_volatile(&__LINK_SECTION) },
|| unsafe { std::ptr::read_volatile(&__LEGACY_LINK_SECTION) }
)
}
}
}
+20 -1
View File
@@ -8,9 +8,26 @@ use quote::ToTokens;
/// After linking, the "manganis" sections of the different object files will be merged.
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
let position = proc_macro2::Span::call_site();
let export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
let export_name = syn::LitStr::new(&format!("__ASSETS__{}", asset_hash), position);
let legacy_export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
quote::quote! {
// We bundle both the legacy and new link sections for compatibility with older CLIs
static __LEGACY_LINK_SECTION: &'static [u8] = {
// First serialize the asset into a constant sized buffer
const __BUFFER: manganis::macro_helpers::const_serialize_07::ConstVec<u8> = manganis::macro_helpers::serialize_asset_07(&#asset);
// Then pull out the byte slice
const __BYTES: &[u8] = __BUFFER.as_ref();
// And the length of the byte slice
const __LEN: usize = __BYTES.len();
// Now that we have the size of the asset, copy the bytes into a static array
#[unsafe(export_name = #legacy_export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
&__LINK_SECTION
};
static __LINK_SECTION: &'static [u8] = {
// First serialize the asset into a constant sized buffer
const __BUFFER: manganis::macro_helpers::const_serialize::ConstVec<u8> = manganis::macro_helpers::serialize_asset(&#asset);
// Then pull out the byte slice
@@ -21,5 +38,7 @@ pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStr
// Now that we have the size of the asset, copy the bytes into a static array
#[unsafe(export_name = #export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
&__LINK_SECTION
};
}
}
+1
View File
@@ -14,6 +14,7 @@ keywords = ["assets"]
[dependencies]
const-serialize = { workspace = true }
const-serialize-07 = { workspace = true }
manganis-core = { workspace = true }
manganis-macro = { workspace = true }
@@ -1,12 +1,15 @@
pub use const_serialize;
use const_serialize::{serialize_const, ConstVec, SerializeConst};
pub use const_serialize_07;
use const_serialize_07::{
serialize_const as serialize_const_07, ConstVec as ConstVec07,
SerializeConst as SerializeConst07,
};
use manganis_core::{AssetOptions, BundledAsset};
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.";
/// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
BundledAsset::new(input_path, PLACEHOLDER_HASH, asset_config)
BundledAsset::new(input_path, BundledAsset::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
@@ -27,7 +30,18 @@ pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
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() {
while data.len() < <BundledAsset as SerializeConst>::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
}
/// Serialize an asset to a const buffer in the legacy 0.7 format
pub const fn serialize_asset_07(asset: &BundledAsset) -> ConstVec07<u8> {
let data = ConstVec07::new();
let mut data = serialize_const_07(asset, data);
// Reserve the maximum size of the asset
while data.len() < <BundledAsset as SerializeConst07>::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
@@ -35,8 +49,7 @@ pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
/// 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()) {
match const_serialize::deserialize_const!(BundledAsset, bytes) {
Some((_, asset)) => asset,
None => panic!("Failed to deserialize asset. This may be caused by a mismatch between your dioxus and dioxus-cli versions"),
}
@@ -1,8 +1,13 @@
// @ts-check
const { test, expect } = require("@playwright/test");
test("optimized scripts run", async ({ page }) => {
await page.goto("http://localhost:8989");
const test_variants = [
{ port: 8989, name: "current version" },
];
for (let { port, name } of test_variants) {
test(`optimized scripts run in ${name}`, async ({ page }) => {
await page.goto(`http://localhost:${port}`);
// // Expect the page to load the script after optimizations have been applied. The script
// // should add an editor to the page that shows a main function
@@ -31,23 +36,23 @@ test("optimized scripts run", async ({ page }) => {
const src3 = await image3.getAttribute("src");
// Expect the src to be without a hash
expect(src3).toEqual("/assets/toasts.avif");
});
});
test("unused external assets are bundled", async ({ page }) => {
await page.goto("http://localhost:8989");
test(`unused external assets are bundled in ${name}`, async ({ page }) => {
await page.goto(`http://localhost:${port}`);
// Assert http://localhost:8989/assets/toasts.png is found even though it is not used in the page
// Assert http://localhost:{port}/assets/toasts.png is found even though it is not used in the page
const response = await page.request.get(
"http://localhost:8989/assets/toasts.png"
`http://localhost:${port}/assets/toasts.png`
);
// Expect the response to be ok
expect(response.status()).toBe(200);
// make sure the response is an image
expect(response.headers()["content-type"]).toBe("image/png");
});
});
test("assets are resolved", async ({ page }) => {
await page.goto("http://localhost:8989");
test(`assets are resolved in ${name}`, async ({ page }) => {
await page.goto(`http://localhost:${port}`);
// Expect the page to contain an element with the id "resolved-data"
const resolvedData = page.locator("#resolved-data");
@@ -56,4 +61,6 @@ test("assets are resolved", async ({ page }) => {
await expect(resolvedData).toContainText(
"List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
);
});
});
}
@@ -2,3 +2,4 @@ dist
target
monaco-editor
partial-monaco-editor
dx-07
@@ -15,3 +15,8 @@ serde_json.workspace = true
# reqwest = { workspace = true, features = ["blocking"] }
# flate2 = "1.1.2"
# tar = "0.4.44"
# We need a separate bin for the old CLI build to avoid conflicting caches
[[bin]]
name = "cli-optimization-old-cli"
path = "src/old_cli.rs"
@@ -0,0 +1,93 @@
// This test checks the CLI optimizes assets correctly without breaking them
use dioxus::prelude::*;
const SOME_IMAGE: Asset = asset!("/images/toasts.png", AssetOptions::image().with_avif());
const SOME_IMAGE_WITH_THE_SAME_URL: Asset =
asset!("/images/toasts.png", AssetOptions::image().with_jpg());
#[used]
static SOME_IMAGE_WITHOUT_HASH: Asset = asset!(
"/images/toasts.png",
AssetOptions::image().with_avif().with_hash_suffix(false)
);
// This asset is unused, but it should still be bundled because it is an external asset
#[used]
static _ASSET: Asset = asset!(
"/images/toasts.png",
AssetOptions::builder().with_hash_suffix(false)
);
pub fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
// todo: test monaco more....
// const MONACO_FOLDER: Asset = asset!("/monaco-editor/package/min/vs");
// let script = format!("(() => {{
// require.config({{ paths: {{ vs: '{MONACO_FOLDER}' }} }});
// require(['vs/editor/editor.main'], () => {{
// var model = monaco.editor.createModel('fn main() {{\\n\\tprintln!(\\\"hi\\\")\\n}}', 'rust');
// var editor = monaco.editor.create(document.getElementById('editor'));
// editor.setModel(model);
// }})
// }})()");
rsx! {
div {
id: "editor",
width: "100vw",
height: "100vw",
}
// // Monaco script
// script {
// src: "{MONACO_FOLDER}/loader.js",
// "onload": script
// }
img {
id: "some_image",
src: "{SOME_IMAGE}"
}
img {
id: "some_image_with_the_same_url",
src: "{SOME_IMAGE_WITH_THE_SAME_URL}"
}
img {
id: "some_image_without_hash",
src: "{SOME_IMAGE_WITHOUT_HASH}"
}
LoadsAsset {}
}
}
const JSON: Asset = asset!("/assets/data.json");
#[derive(Debug, Clone, serde::Deserialize)]
struct Data {
list: Vec<i32>,
}
#[component]
fn LoadsAsset() -> Element {
let data = use_resource(|| async {
let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON)
.await
.unwrap();
serde_json::from_slice::<Data>(&bytes).unwrap()
});
match data() {
Some(data) => rsx! {
div {
id: "resolved-data",
"List: {data.list:?}"
}
},
None => rsx! {
div {
"Loading..."
}
},
}
}
@@ -1,93 +1,3 @@
// This test checks the CLI optimizes assets correctly without breaking them
use dioxus::prelude::*;
const SOME_IMAGE: Asset = asset!("/images/toasts.png", AssetOptions::image().with_avif());
const SOME_IMAGE_WITH_THE_SAME_URL: Asset =
asset!("/images/toasts.png", AssetOptions::image().with_jpg());
#[used]
static SOME_IMAGE_WITHOUT_HASH: Asset = asset!(
"/images/toasts.png",
AssetOptions::image().with_avif().with_hash_suffix(false)
);
// This asset is unused, but it should still be bundled because it is an external asset
#[used]
static _ASSET: Asset = asset!(
"/images/toasts.png",
AssetOptions::builder().with_hash_suffix(false)
);
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
// todo: test monaco more....
// const MONACO_FOLDER: Asset = asset!("/monaco-editor/package/min/vs");
// let script = format!("(() => {{
// require.config({{ paths: {{ vs: '{MONACO_FOLDER}' }} }});
// require(['vs/editor/editor.main'], () => {{
// var model = monaco.editor.createModel('fn main() {{\\n\\tprintln!(\\\"hi\\\")\\n}}', 'rust');
// var editor = monaco.editor.create(document.getElementById('editor'));
// editor.setModel(model);
// }})
// }})()");
rsx! {
div {
id: "editor",
width: "100vw",
height: "100vw",
}
// // Monaco script
// script {
// src: "{MONACO_FOLDER}/loader.js",
// "onload": script
// }
img {
id: "some_image",
src: "{SOME_IMAGE}"
}
img {
id: "some_image_with_the_same_url",
src: "{SOME_IMAGE_WITH_THE_SAME_URL}"
}
img {
id: "some_image_without_hash",
src: "{SOME_IMAGE_WITHOUT_HASH}"
}
LoadsAsset {}
}
}
const JSON: Asset = asset!("/assets/data.json");
#[derive(Debug, Clone, serde::Deserialize)]
struct Data {
list: Vec<i32>,
}
#[component]
fn LoadsAsset() -> Element {
let data = use_resource(|| async {
let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON)
.await
.unwrap();
serde_json::from_slice::<Data>(&bytes).unwrap()
});
match data() {
Some(data) => rsx! {
div {
id: "resolved-data",
"List: {data.list:?}"
}
},
None => rsx! {
div {
"Loading..."
}
},
}
dioxus_cli_optimization_test::main()
}
@@ -0,0 +1,3 @@
fn main() {
dioxus_cli_optimization_test::main()
}
+12 -12
View File
@@ -9,17 +9,17 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.53.1"
"@playwright/test": "^1.56.1"
}
},
"node_modules/@playwright/test": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
"integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.53.1"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -44,13 +44,13 @@
}
},
"node_modules/playwright": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.53.1"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -63,9 +63,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
"integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
+1 -1
View File
@@ -12,6 +12,6 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.53.1"
"@playwright/test": "^1.56.1"
}
}
@@ -144,7 +144,6 @@ if (process.platform === "win32") {
},
{
cwd: path.join(process.cwd(), "cli-optimization"),
// Remove the cache folder for the cli-optimization build to force a full cache reset
command:
'cargo run --package dioxus-cli --release -- run --addr "127.0.0.1" --port 8989',
port: 8989,