use std::{
fs::OpenOptions,
io::{Cursor, Read, Seek, Write},
path::{Path, PathBuf},
time::{Duration, Instant},
};
use crate::opt::AppManifest;
use crate::Result;
use anyhow::{bail, Context};
use const_serialize::{deserialize_const, serialize_const, ConstVec};
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
use manganis_core::SymbolData;
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
use pdb::FallibleIterator;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
#[derive(Copy, Clone)]
enum ManganisVersion {
Legacy,
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 => 4096,
}
}
fn deserialize(&self, data: &[u8]) -> Option<SymbolDataOrAsset> {
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(SymbolDataOrAsset::Asset(legacy_asset_to_modern_asset(
&legacy_asset,
)))
}
ManganisVersion::New => {
if let Some((remaining, symbol_data)) = deserialize_const!(SymbolData, data) {
let is_valid = remaining.is_empty()
|| remaining.iter().all(|&b| b == 0)
|| remaining.len() <= data.len();
if is_valid {
return Some(SymbolDataOrAsset::SymbolData(Box::new(symbol_data)));
} else {
tracing::debug!(
"SymbolData deserialized but invalid padding: {} remaining bytes out of {} total (first few bytes: {:?})",
remaining.len(),
data.len(),
&data[..data.len().min(32)]
);
}
} else {
tracing::debug!(
"Failed to deserialize as SymbolData. Data length: {}, first few bytes: {:?}",
data.len(),
&data[..data.len().min(32)]
);
}
if let Some((remaining, asset)) = deserialize_const!(BundledAsset, data) {
let is_valid = remaining.is_empty() || remaining.iter().all(|&b| b == 0);
if is_valid {
tracing::debug!(
"Successfully deserialized BundledAsset, remaining padding: {} bytes",
remaining.len()
);
return Some(SymbolDataOrAsset::Asset(asset));
} else {
tracing::warn!(
"BundledAsset deserialized but remaining bytes are not all zeros: {} remaining bytes, first few: {:?}",
remaining.len(),
&remaining[..remaining.len().min(16)]
);
}
} else {
tracing::warn!(
"Failed to deserialize as BundledAsset. Data length: {}, first 32 bytes: {:?}",
data.len(),
&data[..data.len().min(32)]
);
}
None
}
}
}
fn serialize_asset(&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());
let mut data = buffer.as_ref().to_vec();
if data.len() < 4096 {
data.resize(4096, 0);
}
data
}
}
}
fn serialize_symbol_data(&self, data: &SymbolData) -> Option<Vec<u8>> {
match self {
ManganisVersion::Legacy => None,
ManganisVersion::New => {
let buffer = serialize_const(data, ConstVec::new());
let mut bytes = buffer.as_ref().to_vec();
if bytes.len() < 4096 {
bytes.resize(4096, 0);
}
Some(bytes)
}
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum SymbolDataOrAsset {
SymbolData(Box<SymbolData>),
Asset(BundledAsset),
}
#[derive(Clone, Copy)]
struct AssetWriteEntry {
symbol: ManganisSymbolOffset,
asset_index: usize,
representation: AssetRepresentation,
}
impl AssetWriteEntry {
fn new(
symbol: ManganisSymbolOffset,
asset_index: usize,
representation: AssetRepresentation,
) -> Self {
Self {
symbol,
asset_index,
representation,
}
}
}
#[derive(Clone, Copy)]
enum AssetRepresentation {
RawBundled,
SymbolData,
}
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
}
}
#[derive(Clone, Copy)]
struct ManganisSymbolOffset {
version: ManganisVersion,
offset: u64,
}
impl ManganisSymbolOffset {
fn new(version: ManganisVersion, offset: u64) -> Self {
Self { version, offset }
}
}
fn find_symbol_offsets<'a, R: ReadRef<'a>>(
path: &Path,
file_contents: &[u8],
file: &File<'a, R>,
) -> Result<Vec<ManganisSymbolOffset>> {
let pdb_file = find_pdb_file(path);
match file.format() {
object::BinaryFormat::Wasm => find_wasm_symbol_offsets(file_contents, file),
object::BinaryFormat::Pe if pdb_file.is_some() => {
find_pdb_symbol_offsets(&pdb_file.unwrap())
}
_ => find_native_symbol_offsets(file),
}
}
fn find_pdb_file(path: &Path) -> Option<PathBuf> {
let mut pdb_file = path.with_extension("pdb");
if let Some(file_name) = pdb_file.file_name() {
let new_file_name = file_name.to_string_lossy().replace('-', "_");
let altrnate_pdb_file = pdb_file.with_file_name(new_file_name);
match (pdb_file.metadata(), altrnate_pdb_file.metadata()) {
(Ok(pdb_metadata), Ok(alternate_metadata)) => {
if let (Ok(pdb_modified), Ok(alternate_modified)) =
(pdb_metadata.modified(), alternate_metadata.modified())
{
if pdb_modified < alternate_modified {
pdb_file = altrnate_pdb_file;
}
}
}
(Err(_), Ok(_)) => {
pdb_file = altrnate_pdb_file;
}
_ => {}
}
}
if pdb_file.exists() {
Some(pdb_file)
} else {
None
}
}
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 {
tracing::error!("Failed to read sections from PDB file");
return Ok(Vec::new());
};
let global_symbols = pdb_file
.global_symbols()
.context("Failed to read global symbols from PDB file")?;
let address_map = pdb_file
.address_map()
.context("Failed to read address map from PDB file")?;
let mut symbols = global_symbols.iter();
let mut addresses = Vec::new();
while let Ok(Some(symbol)) = symbols.next() {
let Ok(pdb::SymbolData::Public(data)) = symbol.parse() else {
continue;
};
let Some(rva) = data.offset.to_section_offset(&address_map) else {
continue;
};
let name = data.name.to_string();
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(ManganisSymbolOffset::new(
version,
(section.pointer_to_raw_data + rva.offset) as u64,
));
}
}
Ok(addresses)
}
fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(
file: &File<'a, R>,
) -> Result<Vec<ManganisSymbolOffset>> {
let mut offsets = Vec::new();
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 __ASSETS__ symbol {:?} in section {}, but the section has no file range",
symbol.name(),
section.index()
);
continue;
};
let section_relative_address: u64 = (virtual_address as i128 - section.address() as i128)
.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(ManganisSymbolOffset::new(version, file_offset));
}
Ok(offsets)
}
fn eval_walrus_global_expr(module: &walrus::Module, expr: &walrus::ConstExpr) -> Option<u64> {
match expr {
walrus::ConstExpr::Value(walrus::ir::Value::I32(value)) => Some(*value as u64),
walrus::ConstExpr::Value(walrus::ir::Value::I64(value)) => Some(*value as u64),
walrus::ConstExpr::Global(id) => {
let global = module.globals.get(*id);
if let walrus::GlobalKind::Local(pointer) = &global.kind {
eval_walrus_global_expr(module, pointer)
} else {
None
}
}
_ => None,
}
}
fn find_global_export_value(module: &walrus::Module, name: &str) -> Option<u64> {
for export in module.exports.iter() {
if export.name == name {
if let walrus::ExportItem::Global(g) = export.item {
if let walrus::GlobalKind::Local(expr) = &module.globals.get(g).kind {
return eval_walrus_global_expr(module, expr);
}
}
}
}
None
}
fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
file_contents: &[u8],
file: &File<'a, R>,
) -> Result<Vec<ManganisSymbolOffset>> {
let Some(section) = file
.sections()
.find(|section| section.name() == Ok("<data>"))
else {
tracing::error!("Failed to find <data> section in WASM file");
return Ok(Vec::new());
};
let Some((_, section_range_end)) = section.file_range() else {
tracing::error!("Failed to find file range for <data> section in WASM file");
return Ok(Vec::new());
};
let section_size = section.data()?.len() as u64;
let section_start = section_range_end - section_size;
let reader = wasmparser::DataSectionReader::new(wasmparser::BinaryReader::new(
&file_contents[section_start as usize..section_range_end as usize],
0,
))
.context("Failed to create WASM data section reader")?;
let mut segment_file_info: Vec<(u64, u64)> = Vec::new();
for segment in reader.into_iter() {
let segment = segment.context("Failed to read data segment")?;
segment_file_info.push((
(segment.data.as_ptr() as u64)
.checked_sub(file_contents.as_ptr() as u64)
.expect("Data segment should be within file contents"),
segment.data.len() as u64,
));
}
if segment_file_info.is_empty() {
return Ok(Vec::new());
}
let module = walrus::Module::from_buffer(file_contents)
.context("Failed to parse WASM module with walrus")?;
let main_memory_walrus = module
.data
.iter()
.next()
.context("Failed to find main memory in WASM module")?;
let main_memory_offset = match &main_memory_walrus.kind {
walrus::DataKind::Active { offset, .. } => {
eval_walrus_global_expr(&module, offset).unwrap_or_default()
}
walrus::DataKind::Passive => {
let memory_base = find_global_export_value(&module, "__memory_base");
let tls_size = find_global_export_value(&module, "__tls_size").unwrap_or(0);
if tls_size > 0 && !segment_file_info.is_empty() && segment_file_info[0].1 == tls_size {
segment_file_info.remove(0);
}
let tls_aligned = (tls_size + 3) & !3;
memory_base.unwrap_or(0x100000u64) + tls_aligned
}
};
let mut offsets = Vec::new();
for export in module.exports.iter() {
let Some(version) = looks_like_manganis_symbol(&export.name) else {
continue;
};
let walrus::ExportItem::Global(global) = export.item else {
continue;
};
let global_data = module.globals.get(global);
let walrus::GlobalKind::Local(pointer) = global_data.kind else {
continue;
};
let Some(virtual_address) = eval_walrus_global_expr(&module, &pointer) else {
tracing::error!(
"Found __ASSETS__ symbol {:?} in WASM file, but the global expression could not be evaluated",
export.name
);
continue;
};
let data_relative_offset =
match (virtual_address as i128).checked_sub(main_memory_offset as i128) {
Some(offset) if offset >= 0 => offset as u64,
_ => {
tracing::error!(
"Virtual address 0x{:x} is below main memory offset 0x{:x}",
virtual_address,
main_memory_offset
);
continue;
}
};
let mut cumulative_offset = 0u64;
let mut file_offset = None;
for (seg_file_offset, seg_size) in segment_file_info.iter() {
if data_relative_offset < cumulative_offset + seg_size {
let offset_in_segment = data_relative_offset - cumulative_offset;
file_offset = Some(seg_file_offset + offset_in_segment);
break;
}
cumulative_offset += seg_size;
}
let Some(file_offset) = file_offset else {
tracing::error!(
"Virtual address 0x{:x} is beyond all data segments",
virtual_address
);
continue;
};
offsets.push(ManganisSymbolOffset::new(version, file_offset));
}
Ok(offsets)
}
pub(crate) async fn extract_symbols_from_file(path: impl AsRef<Path>) -> Result<AppManifest> {
let path = path.as_ref();
let mut file =
open_file_for_writing_with_timeout(path, OpenOptions::new().write(true).read(true)).await?;
let mut file_contents = Vec::new();
file.read_to_end(&mut file_contents)?;
let (offsets, obj_format) = {
let mut reader = Cursor::new(&file_contents);
let read_cache = ReadCache::new(&mut reader);
let object_file = object::File::parse(&read_cache)?;
(
find_symbol_offsets(path, &file_contents, &object_file)?,
object_file.format(),
)
};
let mut assets = Vec::new();
let mut android_artifacts = Vec::new();
let mut swift_packages = Vec::new();
let mut write_entries = Vec::new();
for symbol in offsets.iter().copied() {
let version = symbol.version;
let offset = symbol.offset;
let buffer_size = version
.size()
.min(file_contents.len().saturating_sub(offset as usize));
if buffer_size == 0 {
tracing::warn!("Symbol at offset {offset} is beyond file size");
continue;
}
let data_in_range = if (offset as usize) + buffer_size <= file_contents.len() {
&file_contents[offset as usize..(offset as usize) + buffer_size]
} else {
&file_contents[offset as usize..]
};
if let Some(result) = version.deserialize(data_in_range) {
match result {
SymbolDataOrAsset::SymbolData(symbol_data) => match *symbol_data {
SymbolData::Asset(asset) => {
tracing::debug!(
"Found asset (via SymbolData) at offset {offset}: {:?}",
asset.absolute_source_path()
);
let asset_index = assets.len();
assets.push(asset);
write_entries.push(AssetWriteEntry::new(
symbol,
asset_index,
AssetRepresentation::SymbolData,
));
}
SymbolData::AndroidArtifact(meta) => {
tracing::debug!(
"Found Android artifact declaration for plugin {}",
meta.plugin_name.as_str()
);
android_artifacts.push(meta);
}
SymbolData::SwiftPackage(meta) => {
tracing::debug!(
"Found Swift package declaration for plugin {}",
meta.plugin_name.as_str()
);
swift_packages.push(meta);
}
_ => {}
},
SymbolDataOrAsset::Asset(asset) => {
tracing::debug!(
"Found asset (old format) at offset {offset}: {:?}",
asset.absolute_source_path()
);
let asset_index = assets.len();
assets.push(asset);
write_entries.push(AssetWriteEntry::new(
symbol,
asset_index,
AssetRepresentation::RawBundled,
));
}
}
} else {
tracing::warn!("Found a symbol at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions, or the symbol may be in an unsupported format.");
}
}
assets
.par_iter_mut()
.for_each(crate::opt::add_hash_to_asset);
for entry in write_entries {
let version = entry.symbol.version;
let offset = entry.symbol.offset;
let asset = assets
.get(entry.asset_index)
.copied()
.expect("asset index collected from symbol scan");
match entry.representation {
AssetRepresentation::RawBundled => {
tracing::debug!(
"Writing asset to offset {offset}: {:?} - {:?}",
asset.absolute_source_path(),
asset.bundled_path()
);
let new_data = version.serialize_asset(&asset);
if new_data.len() > version.size() {
tracing::warn!(
"Asset at offset {offset} serialized to {} bytes, but buffer is only {} bytes. Truncating output.",
new_data.len(),
version.size()
);
}
write_serialized_bytes(&mut file, offset, &new_data, version.size())?;
}
AssetRepresentation::SymbolData => {
tracing::debug!("Writing asset (SymbolData) to offset {offset}: {:?}", asset);
let Some(new_data) = version.serialize_symbol_data(&SymbolData::Asset(asset))
else {
tracing::warn!(
"Symbol at offset {offset} was stored as SymbolData but the binary format only supports raw assets"
);
continue;
};
if new_data.len() > version.size() {
tracing::warn!(
"SymbolData asset at offset {offset} serialized to {} bytes, but buffer is only {} bytes. Truncating output.",
new_data.len(),
version.size()
);
}
write_serialized_bytes(&mut file, offset, &new_data, version.size())?;
}
}
}
file.sync_all()
.context("Failed to sync file after writing assets")?;
if obj_format == object::BinaryFormat::MachO && !assets.is_empty() {
let output = tokio::process::Command::new("codesign")
.arg("--force")
.arg("--sign")
.arg("-") .arg(path)
.output()
.await
.context("Failed to run codesign - is `codesign` in your path?")?;
if !output.status.success() {
bail!(
"Failed to re-sign the binary with codesign after finalizing the assets: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
let mut manifest = AppManifest::new();
for asset in assets {
manifest.insert_asset(asset);
}
manifest.android_artifacts = android_artifacts;
manifest.swift_sources = swift_packages;
Ok(manifest)
}
async fn open_file_for_writing_with_timeout(
file: &Path,
options: &mut OpenOptions,
) -> Result<std::fs::File> {
let start_time = Instant::now();
let timeout = Duration::from_secs(5);
loop {
match options.open(file) {
Ok(file) => return Ok(file),
Err(e) => {
if cfg!(windows) && e.raw_os_error() == Some(32) && start_time.elapsed() < timeout {
tracing::trace!(
"Failed to open file because another process is using it. Retrying..."
);
tokio::time::sleep(Duration::from_millis(50)).await;
} else {
return Err(e.into());
}
}
}
}
}
fn write_serialized_bytes(
file: &mut std::fs::File,
offset: u64,
data: &[u8],
buffer_size: usize,
) -> Result<()> {
use std::io::SeekFrom;
file.seek(SeekFrom::Start(offset))?;
if data.len() <= buffer_size {
file.write_all(data)?;
if data.len() < buffer_size {
let padding = vec![0; buffer_size - data.len()];
file.write_all(&padding)?;
}
} else {
file.write_all(&data[..buffer_size])?;
}
Ok(())
}
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
file: &'b File<'a, R>,
) -> 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((version, symbol, section))
})
}