Skip to content

Commit 0f45951

Browse files
committed
Use custom code for WASM custom sections
Use our own code to read/write WASM custom sections. This allows us to remove `wasm-gen` as a dependency, and also to support removing custom sections with `set-metadata`.
1 parent 76cd232 commit 0f45951

File tree

6 files changed

+270
-57
lines changed

6 files changed

+270
-57
lines changed

Cargo.lock

Lines changed: 2 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ serde_json5 = "0.2.1"
3838
tokio = { version = "1.43.0", features = ["rt", "macros", "fs"] }
3939
tokio-util = { version = "0.7.13", features = ["io",] }
4040
walkdir = "2.5.0"
41-
wasm-gen = "0.1.4"
42-
wasmtime = { version = "32.0.0", features = ["reexport-wasmparser"] }
41+
wasmtime = { version = "32.0.0" }
4342
wasmtime-wasi = "32.0.0"
4443

4544
# OpenSSL is only used on Unix. We don't want to add it as a dependency on Windows.

src/leb128.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/// Convert a u32 to LEB128 encoding. This is used in the WASM binary format.
2+
/// I got a little bit carried away here making a version without many branches.
3+
/// I have exhaustively tested that it is correct using the `exhaustive_leb128_test()`
4+
/// test below.
5+
pub fn u32_to_leb128(n: u32) -> Vec<u8> {
6+
let mut leb = vec![
7+
((n >> 0) as u8 & 0x7F) | (if n >> 7 == 0 { 0 } else { 0x80 }),
8+
((n >> 7) as u8 & 0x7F) | (if n >> 14 == 0 { 0 } else { 0x80 }),
9+
((n >> 14) as u8 & 0x7F) | (if n >> 21 == 0 { 0 } else { 0x80 }),
10+
((n >> 21) as u8 & 0x7F) | (if n >> 28 == 0 { 0 } else { 0x80 }),
11+
((n >> 28) as u8 & 0x7F),
12+
];
13+
let num_bytes = n.checked_ilog2().map(|n: u32| n / 7 + 1).unwrap_or(1);
14+
leb.resize(num_bytes as usize, 0);
15+
leb
16+
}
17+
18+
/// Convert leb128 to u32, returning the value and number of bytes read.
19+
/// If there are insufficient bytes we return None.
20+
pub fn leb128_to_u32(leb: &[u8]) -> Option<(u32, usize)> {
21+
let mut n = 0u32;
22+
for i in 0..5 {
23+
let byte = leb.get(i)?;
24+
n |= ((byte & 0x7F) as u32) << (i * 7);
25+
if byte & 0x80 == 0 {
26+
return Some((n, i + 1));
27+
}
28+
}
29+
Some((n, 5))
30+
}
31+
32+
#[cfg(test)]
33+
mod test {
34+
use super::*;
35+
36+
fn u32_to_leb128_simple(n: u32) -> Vec<u8> {
37+
let mut leb = Vec::with_capacity(5);
38+
39+
for septet in 0..5 {
40+
let byte = (n >> (7 * septet)) as u8 & 0x7F;
41+
if septet == 4 || (n >> (7 * (septet + 1))) == 0 {
42+
// Last byte.
43+
leb.push(byte);
44+
break;
45+
} else {
46+
leb.push(byte | 0x80);
47+
}
48+
}
49+
leb
50+
}
51+
52+
/// To run:
53+
///
54+
/// cargo test --release -- --ignored exhaustive_leb128_test
55+
///
56+
/// It takes about 5 minutes.
57+
#[test]
58+
#[ignore]
59+
fn exhaustive_leb128_test() {
60+
for i in 0..=u32::MAX {
61+
let correct_encoding = u32_to_leb128_simple(i);
62+
assert_eq!(u32_to_leb128(i), correct_encoding);
63+
assert!(matches!(leb128_to_u32(&correct_encoding), Some((v, _)) if v == i));
64+
}
65+
}
66+
}

src/main.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ mod fetch;
55
mod file_matching;
66
mod git;
77
mod hash_adapter;
8+
mod leb128;
89
mod metadata;
910
mod serde_glob;
1011
mod serde_regex;
1112
mod unique_filename;
1213
mod wasi_cache;
14+
mod wasm;
1315

1416
use anyhow::{Result, anyhow, bail};
1517
use bash_paths::path_to_bash_string;
@@ -21,10 +23,11 @@ use fetch::fetch_linters;
2123
use file_matching::retain_matching_files;
2224
use git::git_diff_unstaged;
2325
use log::info;
24-
use metadata::{has_metadata, read_metadata};
26+
use metadata::read_metadata;
2527
use owo_colors::OwoColorize;
2628
use std::path::{Path, PathBuf};
2729
use tokio::fs;
30+
use wasm::{find_custom_sections, make_custom_section};
2831

2932
#[derive(Parser)]
3033
#[command(
@@ -358,16 +361,20 @@ async fn subcommand_show_metadata(_cli: &Cli, args: &ShowMetadataArgs) -> Result
358361
}
359362

360363
async fn subcommand_set_metadata(_cli: &Cli, args: &SetMetadataArgs) -> Result<()> {
361-
// TODO (1.0): Remove any existing custom metadata sections.
362-
363364
let mut bytes = fs::read(&args.file).await?;
364-
if has_metadata(&bytes)? {
365-
bail!("File already has metadata. Removing it is not yet supported.");
366-
}
367365
let metadata_bytes = fs::read(&args.metadata).await?;
368366

369-
// TODO (1.0): This is simple enough we can do it without an external crate.
370-
wasm_gen::write_custom_section(&mut bytes, "nit_metadata", &metadata_bytes);
367+
// Find the existing metadata sections.
368+
let (section_ranges, _) = find_custom_sections(&bytes, "nit_metadata")?;
369+
370+
// Remove them all.
371+
for range in section_ranges.into_iter().rev() {
372+
bytes.drain(range);
373+
}
374+
375+
// Add a new section on the end.
376+
let metadata_section = make_custom_section("nit_metadata", &metadata_bytes);
377+
bytes.extend_from_slice(&metadata_section);
371378

372379
fs::write(&args.file, bytes).await?;
373380

src/metadata.rs

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use anyhow::{Context, Result, anyhow, bail};
22
use serde::Deserialize;
33
use std::path::Path;
4-
use wasmtime::wasmparser::{Parser, Payload};
54

6-
use crate::file_matching::MatchExpression;
5+
use crate::{file_matching::MatchExpression, wasm::find_custom_sections};
76

87
#[derive(Debug, Deserialize)]
98
pub struct ArgBlock {
@@ -53,35 +52,21 @@ pub struct NitMetadata {
5352
/// cargo install wasm-custom-section
5453
///
5554
pub fn read_metadata(wasm_path: &Path) -> Result<NitMetadata> {
56-
let module = std::fs::read(wasm_path)?;
55+
let wasm_bytes = std::fs::read(wasm_path)?;
5756

5857
// Ideally we wouldn't load the entire file into memory, but
59-
// it's probably fine in most cases and wasmparser doesn't provide
60-
// a handy parse_reader() method.
58+
// it's probably fine in most cases.
6159

62-
let parser = Parser::new(0);
63-
for payload in parser.parse_all(&module) {
64-
match payload? {
65-
Payload::CustomSection(section) if section.name() == "nit_metadata" => {
66-
return Ok(serde_json::from_slice::<NitMetadata>(section.data())
67-
.with_context(|| anyhow!("Reading metadata for {}", wasm_path.display()))?);
68-
}
69-
_ => {}
70-
}
71-
}
72-
bail!("No nit_metadata section found in the wasm file");
73-
}
60+
let (_, section_contents) = find_custom_sections(&wasm_bytes, "nit_metadata")
61+
.context("Finding nit_metadata section")?;
7462

75-
/// Return true if the file has a section called `nit_metadata`.
76-
pub fn has_metadata(module: &[u8]) -> Result<bool> {
77-
let parser = Parser::new(0);
78-
for payload in parser.parse_all(&module) {
79-
match payload? {
80-
Payload::CustomSection(section) if section.name() == "nit_metadata" => {
81-
return Ok(true);
82-
}
83-
_ => {}
84-
}
63+
if section_contents.is_empty() {
64+
bail!("No nit_metadata section found in the wasm file");
8565
}
86-
Ok(false)
66+
if section_contents.len() > 1 {
67+
bail!("Multiple nit_metadata sections found in the wasm file");
68+
}
69+
70+
Ok(serde_json::from_slice::<NitMetadata>(section_contents[0])
71+
.with_context(|| anyhow!("Reading metadata for {}", wasm_path.display()))?)
8772
}

0 commit comments

Comments
 (0)