#![warn(missing_docs)]
mod error;
#[cfg(test)]
mod test;
use error::Error;
use object::{Object, ObjectSection};
use std::{
borrow::Cow,
collections::BTreeMap,
fs,
io::{self, Seek, Write},
ops::Deref,
path::{Path, PathBuf},
};
const DWARF_CODE_SECTION_ID: usize = 10;
#[derive(Debug)]
pub struct CodePoint {
path: PathBuf,
address: i64,
line: i64,
column: i64,
}
#[derive(Debug)]
pub struct WASM {
path: PathBuf,
points: BTreeMap<i64, CodePoint>,
sourcemap_size: Option<u64>,
}
impl WASM {
pub fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref().to_owned();
#[cfg(feature = "memmap2")]
let raw = {
let file = fs::File::open(&path)?;
unsafe { memmap2::Mmap::map(&file) }?
};
#[cfg(not(feature = "memmap2"))]
let raw = {
fs::read(&path)?
};
let object = object::File::parse(raw.deref())?;
let sourcemap_size = match object.section_by_name("sourceMappingURL") {
Some(section) => {
const CUSTOM_SEGMENT_ID_SIZE: u64 = 1;
const SEGMENT_NAME_SIZE: u64 =
std::mem::size_of::<u8>() as u64 + b"sourceMappingURL".len() as u64;
let section_size_length = WASM::encode_uint_var(section.size() as u32).len() as u64;
let section_size = CUSTOM_SEGMENT_ID_SIZE
+ SEGMENT_NAME_SIZE
+ section_size_length
+ section.size();
Some(section_size)
}
None => None,
};
let offset: i64 = {
let (code_section_offset, _) = object
.section_by_index(object::SectionIndex(DWARF_CODE_SECTION_ID))?
.file_range()
.ok_or("Missing code section in WASM")?;
code_section_offset.try_into()?
};
let section =
gimli::Dwarf::load(|id: gimli::SectionId| -> Result<Cow<[u8]>, gimli::Error> {
match object.section_by_name(id.name()) {
Some(ref section) => Ok(section
.uncompressed_data()
.unwrap_or(Cow::Borrowed(&[][..]))),
None => Ok(Cow::Borrowed(&[][..])),
}
})?;
let borrow_section: &dyn for<'a> Fn(
&'a Cow<[u8]>,
)
-> gimli::EndianSlice<'a, gimli::RunTimeEndian> =
&|section| gimli::EndianSlice::new(section, gimli::RunTimeEndian::Little);
let dwarf = section.borrow(&borrow_section);
let mut points: BTreeMap<i64, CodePoint> = BTreeMap::new();
let mut iter = dwarf.units();
while let Some(header) = iter.next()? {
let unit = dwarf.unit(header)?;
if let Some(program) = unit.line_program.clone() {
let mut rows = program.rows();
while let Some((header, row)) = rows.next_row()? {
let mut path = PathBuf::new();
if let Some(file) = row.file(header) {
if file.directory_index() != 0 {
if let Some(dir) = file.directory(header) {
path.push(
dwarf.attr_string(&unit, dir)?.to_string_lossy().as_ref(),
);
}
}
path.push(
dwarf
.attr_string(&unit, file.path_name())?
.to_string_lossy()
.as_ref(),
);
}
let address: i64 = {
let mut addr: i64 = row.address().try_into()?;
if row.end_sequence() {
addr -= 1;
}
addr + offset
};
let line = {
let line = match row.line() {
Some(line) => line.get(),
None => 0,
};
line.try_into()?
};
let column: i64 = {
let col = match row.column() {
gimli::ColumnType::LeftEdge => 1,
gimli::ColumnType::Column(column) => column.get(),
};
col.try_into()?
};
let point = CodePoint {
path,
address,
line,
column,
};
points.insert(point.address, point);
}
}
}
Ok(Self {
path,
points,
sourcemap_size,
})
}
pub fn map_v3(&self) -> String {
let mut sourcemap = String::with_capacity(self.points.len() * 4 + 100);
let (mappings, sources) = self.generate();
sourcemap.push('{');
sourcemap.push_str(r#""version":3,"#);
sourcemap.push_str(r#""names":[],"#);
sourcemap.push_str(format!(r#""sources":["{}"],"#, sources.join(r#"",""#)).as_str());
sourcemap.push_str(r#""sourcesContent":null,"#);
sourcemap.push_str(format!(r#""mappings":"{}""#, mappings.join(",")).as_str());
sourcemap.push('}');
sourcemap
}
#[allow(rustdoc::invalid_html_tags)]
pub fn patch(&mut self, url: &str) -> Result<(), Error> {
let mut wasm = fs::OpenOptions::new()
.write(true)
.open(&self.path)
.map_err(|err| {
format!(
"Failed to open WASM file to append sourcemap section: {}",
err
)
})?;
let size = wasm.seek(io::SeekFrom::End(0))?;
let pos = self
.sourcemap_size
.map(|length| size - length)
.unwrap_or(size);
wasm.set_len(pos)?;
wasm.seek(io::SeekFrom::End(0))?;
const WASM_CUSTOM_SECTION_ID: u32 = 0;
let section_name = "sourceMappingURL";
let section_content = [
&WASM::encode_uint_var(section_name.len() as u32)[..],
section_name.as_bytes(),
&WASM::encode_uint_var(url.len() as u32)[..],
url.as_bytes(),
]
.concat();
let section = [
&WASM::encode_uint_var(WASM_CUSTOM_SECTION_ID)[..],
&WASM::encode_uint_var(section_content.len() as u32)[..],
section_content.as_ref(),
]
.concat();
wasm.write_all(§ion)
.map_err(|err| format!("Failed to write sourcemap section to WASM file: {}", err))?;
let _s = wasm.seek(io::SeekFrom::End(0));
self.sourcemap_size = Some(section.len() as u64);
Ok(())
}
fn generate<'a>(&'a self) -> (Vec<String>, Vec<String>) {
let mut sources: Vec<&'a Path> = Vec::new();
let mut mappings: Vec<String> = Vec::new();
let mut last_address: i64 = 0;
let mut last_source_id: i64 = 0;
let mut last_line: i64 = 1;
let mut last_column: i64 = 1;
for line in self.points.values() {
if line.line == 0 {
continue;
}
let source_id: i64 =
if let Some(id) = sources.iter().position(|&val| val == line.path.as_path()) {
id as i64
} else {
let id = sources.len() as i64;
sources.push(&line.path);
id
};
let address_delta = line.address - last_address;
let source_id_delta = source_id - last_source_id;
let line_delta = line.line - last_line;
let column_delta = line.column - last_column;
let mapping = format!(
"{}{}{}{}",
WASM::vlq_encode(address_delta).as_str(),
WASM::vlq_encode(source_id_delta).as_str(),
WASM::vlq_encode(line_delta).as_str(),
WASM::vlq_encode(column_delta).as_str()
);
mappings.push(mapping);
last_address = line.address;
last_source_id = source_id;
last_line = line.line;
last_column = line.column;
}
let source_paths = sources
.iter()
.filter_map(|p| Some(p.as_os_str().to_str()?.to_owned()))
.collect::<Vec<_>>();
(mappings, source_paths)
}
fn vlq_encode(value: i64) -> String {
const VLQ_CHARS: &[u8] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes();
let mut x = if value >= 0 {
value << 1
} else {
(-value << 1) + 1
};
let mut result = String::new();
while x > 31 {
let idx: usize = (32 + (x & 31)).try_into().unwrap();
let ch: char = VLQ_CHARS[idx].into();
result.push(ch);
x >>= 5;
}
let idx: usize = x.try_into().unwrap();
let ch: char = VLQ_CHARS[idx].into();
result.push(ch);
result
}
fn encode_uint_var(mut n: u32) -> Vec<u8> {
let mut result = Vec::new();
while n > 127 {
result.push((128 | (n & 127)) as u8);
n >>= 7;
}
result.push(n as u8);
result
}
}