How to Assert a Custom Section in a WASM (in JavaScript)

Read and assert a WebAssembly module metadata using Vitest, a quick recipe.

Apr 14, 2025

#wasm #vitest #test #metadata

Photo by Pawel Czerwinski on Unsplash


Not sure if this blog post will ever be read or useful to anyone — feels like one of those super ultra-niche things. And to be honest, it’s nothing groundbreaking either. If you check MDN or ask your favorite AI, you’ll probably figure it out pretty quickly.

But hey, I felt like sharing anyway.

On Juno, every container — or smart contract, or idenpendent microservice (call them what you want) — is a WebAssembly module running on the Internet Computer. While I was building support for serverless functions written in TypeScript, I ran into a need, keeping track of the version of each deployed function written by the developers.

After a few iterations, I landed on an approach where each module gets a pseudo-package.json embedded as a custom section in the WASM binary. This way, any part of the ecosystem — whether tooling or the platform itself — can read the module's version and dependencies.

Once the feature was working, I knew I needed a test for it. Embedding and exposing that metadata is pretty critical. So I wrote a Vitest assertion to validate the metadata, and figured I’d share the solution with you here.


The High-Level Idea

I needed to make sure my WASM modules actually include the custom juno:package section I add during build time — and that the metadata inside it is correct.

So I wrote a simple Vitest test that:

  1. Reads the compiled .wasm file
  2. Extracts the custom section named icp:public juno:package
  3. Parses it as JSON
  4. Asserts the content matches what I expect

Here’s what that looks like:

it('should expose public custom section juno:package', async () => { const junoPkg = await customSectionJunoPackage({ path: SATELLITE_WASM_PATH }); expect(junoPkg).toEqual({ name: '@junobuild/satellite', version: readWasmVersion('satellite') }); });

Reading the Custom Section

The customSectionJunoPackage() helper is just a tiny wrapper that calls a more generic customSection() function and parses the result as JSON:

export const customSectionJunoPackage = async ({ path }) => { const section = await customSection({ path, sectionName: 'icp:public juno:package' }); return JSON.parse(section); };

I wrapped the logic in a helper because — who knows — I might need to read other sections in the future. Also, my section name is prefixed with icp:public because, according to the spec, custom metadata needs that prefix to be exposed publicly on the network.

That said, nothing fancy. The real work happens inside customSection() — that’s where we load the WASM, decompress it (since I gzip it during the build), and extract the actual custom section.

Here’s how that looks:

const customSection = async ({ path, sectionName }: { path: string; sectionName: string; }): Promise<string> => { // Read WASM const buffer = await readFile(path); const wasm = await gunzipFile({ source: buffer }); // Compile a WebAssembly.Module object const wasmModule = await WebAssembly.compile(wasm); // Read the public custom section const pkgSections = WebAssembly.Module.customSections(wasmModule, sectionName); expect(pkgSections).toHaveLength(1); // Parse content to object const [pkgBuffer] = pkgSections; return uint8ArrayToString(pkgBuffer); };

Now let’s walk through it step by step.

  1. Read the .wasm file
import { readFile } from 'fs/promises'; const buffer = await readFile(path);

Pretty straightforward — this reads the file from disk (either absolute or relative path) and gives us a raw buffer.

  1. Decompress it
import { gunzipFile } from '@junobuild/cli-tools'; const wasm = await gunzipFile({ source: buffer });

Since I gzip the .wasm file during the build step (mostly for size reasons), I need to decompress it before I can inspect it. gunzipFile is a small helper I use for this — but under the hood, it just relies on the Node.js zlib API.

Here’s what it looks like:

import {Readable} from 'node:stream'; import {createGunzip} from 'node:zlib'; export const gunzipFile = async ({ source }: { source: Buffer }): Promise<Buffer> => await new Promise((resolve, reject) => { const sourceStream = Readable.from(source); const chunks: Uint8Array[] = []; const gzip = createGunzip(); sourceStream.pipe(gzip); gzip.on('data', (chunk) => chunks.push(chunk)); gzip.on('end', () => resolve(Buffer.concat(chunks))); gzip.on('error', reject); });
  1. Compile it to a WebAssembly module
const wasmModule = await WebAssembly.compile(wasm);

This step turns the decompressed binary into an actual WebAssembly.Module. This is required because, well, we can’t directly inspect sections from raw bytes — we need a compiled module first (MDN docs).

⚠️ We’re not instantiating the module (i.e., not running it), we’re just compiling it so we can poke around inside.

  1. Read the custom section and make sure it exists
const pkgSections = WebAssembly.Module.customSections(wasmModule, sectionName); expect(pkgSections).toHaveLength(1);

Here’s where the magic happens.

The WebAssembly.Module.customSections() method lets you extract any custom sections by name — in my case, the icp:public juno:package key.

Since a WASM module can technically have multiple sections, I get them as an array. That’s why I assert its length to make sure the section is actually there — because if it’s not, something went wrong in the build.

No section? No metadata anyway.

  1. Convert the section to a string
import { uint8ArrayToString } from 'uint8array-extras'; const [pkgBuffer] = pkgSections; return uint8ArrayToString(pkgBuffer);

The section we get is a ArrayBuffer, so the last step is just turning it into a string. In my case, that string is JSON, but at this level, we don’t assume anything — just return it as-is.

Later, in customSectionJunoPackage(), I parse it with JSON.parse(), but this low-level helper could work for any kind of string-based metadata.

For conversion, I use a helper from the uint8array-extras library as I already depend on it in the project since I’m using other CLI tools from the same author.

That’s it. No WebAssembly voodoo here. Just basic file I/O, a bit of decompression, and some native JS/WebAssembly APIs doing their thing.


Bonus: Reading the Original Cargo.toml

You might’ve noticed the readWasmVersion() function back in the test. That’s there to make sure the version embedded in the WASM’s juno:package section is actually the one defined in the original Cargo.toml.

Since my build pipeline grabs the version from that file, I want my test to confirm it’s doing the right thing.

Here’s how I read the version from the TOML:

import { parse } from '@ltd/j-toml'; import { assertNonNullish} from '@dfinity/utils'; export const readWasmVersion = (segment: string): string => { const tomlFile = readFileSync(join(process.cwd(), 'src', segment, 'Cargo.toml')); type Toml = { package: { version: string } } | undefined; const result: Toml = parse(tomlFile.toString()) as unknown as Toml; const version = result?.package?.version; assertNonNullish(version); return version; };

I use the j-toml library to parse TOML in a way that’s faithful enough to trust when bundling and reading and assertNonNullish() is just a handy helper I use across the codebase — it throws if the value is null or undefined.

This way, the test isn’t just checking if the metadata is present — it’s making sure it’s correct.


That’s a Wrap

That’s pretty much it. A small test, a bit of metadata, and some light WASM poking. Again, not sure if this will ever be helpful for anyone — or if anyone will even read it — but if you did, hope it was enjoyable.

Cheers,
David