use anyhow::{Context, anyhow};
use camino::Utf8Path;
use wasm_encoder::reencode::{Error, Reencode, ReencodeComponent};
pub const SLOT_MAGIC: &[u8; 16] = b"WASM_RQJS_SLOT\x01\x00";
pub const SLOT_END_MAGIC: &[u8; 16] = b"WASM_RQJS_SLTND\x00";
const MARKER_SIZE: usize = 40;
const WASM_PAGE_SIZE: u32 = 65536;
pub fn create_marker_file(module_index: u32) -> Vec<u8> {
let mut data = Vec::with_capacity(MARKER_SIZE);
data.extend_from_slice(SLOT_MAGIC);
data.extend_from_slice(&module_index.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(SLOT_END_MAGIC);
data
}
pub fn inject_js_into_component(
input: &Utf8Path,
output: &Utf8Path,
js_sources: &[&str],
) -> anyhow::Result<()> {
let wasm_bytes = std::fs::read(input.as_std_path())
.with_context(|| format!("Failed to read input component: {input}"))?;
let patched = inject_js_into_bytes(&wasm_bytes, js_sources)?;
std::fs::write(output.as_std_path(), &patched)
.with_context(|| format!("Failed to write output component: {output}"))?;
Ok(())
}
pub fn inject_js_into_bytes(wasm_bytes: &[u8], js_sources: &[&str]) -> anyhow::Result<Vec<u8>> {
if js_sources.is_empty() {
return Err(anyhow!("No JS sources provided for injection"));
}
let js_payloads: Vec<Vec<u8>> = js_sources
.iter()
.map(|src| {
let js_bytes = src.as_bytes();
let mut payload = Vec::with_capacity(4 + js_bytes.len());
payload.extend_from_slice(&(js_bytes.len() as u32).to_le_bytes());
payload.extend_from_slice(js_bytes);
payload
})
.collect();
let total_payload_size: usize = js_payloads.iter().map(|p| p.len()).sum();
let mut rewriter = MarkerRewriter {
js_payloads,
total_payload_size,
markers_found: Vec::new(),
max_data_end: 0,
js_mem_offsets: Vec::new(),
original_memory_min: 0,
};
let parser = wasmparser_encoder::Parser::new(0);
let mut component = wasm_encoder::Component::new();
rewriter
.parse_component(&mut component, parser, wasm_bytes)
.map_err(|e| match e {
Error::UserError(e) => e,
Error::ParseError(e) => anyhow!("Failed to parse WASM component: {e}"),
other => anyhow!("Failed to reencode WASM component: {other}"),
})?;
if rewriter.markers_found.is_empty() {
return Err(anyhow!(
"No JS injection markers found in the WASM component. \
Was it compiled with EmbeddingMode::BinarySlot?"
));
}
for i in 0..js_sources.len() as u32 {
if !rewriter.markers_found.contains(&i) {
return Err(anyhow!(
"JS injection marker with MODULE_INDEX={i} not found in the WASM component. \
Expected {expected} markers but only found: {found:?}",
expected = js_sources.len(),
found = rewriter.markers_found,
));
}
}
let mut output = component.finish();
patch_js_offsets_in_output(&mut output, &rewriter.js_mem_offsets)?;
Ok(output)
}
fn is_marker_at(data: &[u8], offset: usize) -> bool {
offset + MARKER_SIZE <= data.len()
&& &data[offset..offset + 16] == SLOT_MAGIC
&& &data[offset + 24..offset + MARKER_SIZE] == SLOT_END_MAGIC
}
fn marker_module_index(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(data[offset + 16..offset + 20].try_into().unwrap())
}
fn marker_js_offset(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(data[offset + 20..offset + 24].try_into().unwrap())
}
fn find_marker_in_data(data: &[u8]) -> Option<usize> {
if data.len() < MARKER_SIZE {
return None;
}
(0..=data.len() - MARKER_SIZE).find(|&i| is_marker_at(data, i))
}
struct MarkerRewriter {
js_payloads: Vec<Vec<u8>>,
total_payload_size: usize,
markers_found: Vec<u32>,
max_data_end: u32,
js_mem_offsets: Vec<(u32, u32)>,
original_memory_min: u32,
}
impl Reencode for MarkerRewriter {
type Error = anyhow::Error;
fn parse_data(
&mut self,
data: &mut wasm_encoder::DataSection,
datum: wasmparser_encoder::Data<'_>,
) -> Result<(), Error<Self::Error>> {
if let wasmparser_encoder::DataKind::Active {
memory_index: 0,
offset_expr,
} = &datum.kind
&& let Some(offset) = eval_const_i32(offset_expr)
{
let end = offset.saturating_add(datum.data.len() as u32);
self.max_data_end = self.max_data_end.max(end);
}
if let Some(marker_offset) = find_marker_in_data(datum.data) {
let module_index = marker_module_index(datum.data, marker_offset);
if self.markers_found.contains(&module_index) {
return Err(Error::UserError(anyhow!(
"Found duplicate JS injection marker with MODULE_INDEX={module_index}"
)));
}
self.markers_found.push(module_index);
}
wasm_encoder::reencode::utils::parse_data(self, data, datum)
}
fn parse_data_section(
&mut self,
data: &mut wasm_encoder::DataSection,
section: wasmparser_encoder::DataSectionReader<'_>,
) -> Result<(), Error<Self::Error>> {
wasm_encoder::reencode::utils::parse_data_section(self, data, section)?;
let mut current_offset =
page_align(self.max_data_end).max(self.original_memory_min * WASM_PAGE_SIZE);
let mut sorted_indices = self.markers_found.clone();
sorted_indices.sort();
for module_index in sorted_indices {
if let Some(payload) = self.js_payloads.get(module_index as usize) {
let offset_expr = wasm_encoder::ConstExpr::i32_const(current_offset as i32);
data.active(0, &offset_expr, payload.iter().copied());
self.js_mem_offsets.push((module_index, current_offset));
current_offset = page_align(current_offset + payload.len() as u32);
}
}
Ok(())
}
fn data_count(&mut self, count: u32) -> Result<u32, Error<Self::Error>> {
Ok(count + self.js_payloads.len() as u32)
}
fn parse_memory_section(
&mut self,
memories: &mut wasm_encoder::MemorySection,
section: wasmparser_encoder::MemorySectionReader<'_>,
) -> Result<(), Error<Self::Error>> {
for memory in section {
let memory = memory.map_err(Error::ParseError)?;
self.original_memory_min = memory.initial as u32;
let max_padding = self.js_payloads.len() as u32 * WASM_PAGE_SIZE;
let js_end_upper = self.original_memory_min * WASM_PAGE_SIZE
+ self.total_payload_size as u32
+ max_padding;
let pages_needed = js_end_upper.div_ceil(WASM_PAGE_SIZE);
let new_min = pages_needed.max(memory.initial as u32);
let new_max = memory.maximum.map(|m| m.max(new_min as u64));
memories.memory(wasm_encoder::MemoryType {
minimum: new_min as u64,
maximum: new_max,
memory64: memory.memory64,
shared: memory.shared,
page_size_log2: memory.page_size_log2,
});
}
Ok(())
}
}
impl ReencodeComponent for MarkerRewriter {}
fn eval_const_i32(expr: &wasmparser_encoder::ConstExpr<'_>) -> Option<u32> {
let mut reader = expr.get_operators_reader();
if let Ok(wasmparser_encoder::Operator::I32Const { value }) = reader.read() {
return Some(value as u32);
}
None
}
fn page_align(addr: u32) -> u32 {
(addr + WASM_PAGE_SIZE - 1) & !(WASM_PAGE_SIZE - 1)
}
fn patch_js_offsets_in_output(output: &mut [u8], offsets: &[(u32, u32)]) -> anyhow::Result<()> {
let mut marker_positions: Vec<(usize, u32)> = Vec::new();
for i in 0..output.len().saturating_sub(MARKER_SIZE) {
if is_marker_at(output, i) {
let module_idx = marker_module_index(output, i);
let js_off = marker_js_offset(output, i);
if js_off == 0 {
marker_positions.push((i, module_idx));
}
}
}
for &(module_index, js_mem_offset) in offsets {
let pos = marker_positions
.iter()
.find(|(_, idx)| *idx == module_index)
.map(|(pos, _)| *pos)
.ok_or_else(|| {
anyhow!(
"Could not find unpatched marker with MODULE_INDEX={module_index} \
in reencoded output"
)
})?;
output[pos + 20..pos + 24].copy_from_slice(&js_mem_offset.to_le_bytes());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_marker_file() {
let marker = create_marker_file(0);
assert_eq!(marker.len(), MARKER_SIZE);
assert_eq!(&marker[..16], SLOT_MAGIC.as_slice());
assert_eq!(u32::from_le_bytes(marker[16..20].try_into().unwrap()), 0); assert_eq!(u32::from_le_bytes(marker[20..24].try_into().unwrap()), 0); assert_eq!(&marker[24..], SLOT_END_MAGIC.as_slice());
let marker1 = create_marker_file(1);
assert_eq!(u32::from_le_bytes(marker1[16..20].try_into().unwrap()), 1);
assert_eq!(u32::from_le_bytes(marker1[20..24].try_into().unwrap()), 0);
}
#[test]
fn test_find_marker_in_data() {
let marker = create_marker_file(0);
assert_eq!(find_marker_in_data(&marker), Some(0));
let mut data = vec![0xAA; 100];
data.extend_from_slice(&marker);
data.extend_from_slice(&[0xBB; 50]);
assert_eq!(find_marker_in_data(&data), Some(100));
assert_eq!(find_marker_in_data(&[0u8; 100]), None);
assert_eq!(find_marker_in_data(&[0u8; 10]), None);
}
#[test]
fn test_inject_no_marker() {
let component = wasm_encoder::Component::new();
let bytes = component.finish();
let result = inject_js_into_bytes(&bytes, &["x"]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("No JS injection markers found")
);
}
#[test]
fn test_page_align() {
assert_eq!(page_align(0), 0);
assert_eq!(page_align(1), WASM_PAGE_SIZE);
assert_eq!(page_align(WASM_PAGE_SIZE), WASM_PAGE_SIZE);
assert_eq!(page_align(WASM_PAGE_SIZE + 1), 2 * WASM_PAGE_SIZE);
}
}