current commit
260922.6 · Add a utility for handling modules embedded in JS (#9119)
/*
 * Copyright 2026 WebAssembly Community Group participants
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <string>
#include <vector>

#include "support/js-embedded-module.h"
#include "gtest/gtest.h"

using namespace wasm;

namespace {

std::vector<char> makeBytes(std::initializer_list<uint8_t> list) {
  std::vector<char> result;
  result.reserve(list.size());
  for (uint8_t b : list) {
    result.push_back(static_cast<char>(b));
  }
  return result;
}

} // namespace

TEST(JsEmbeddedModuleTest, HexArrayLiteral) {
  std::string js = R"(
    let bytes = new Uint8Array([
      0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00
    ]);
  )";

  auto modules = findEmbeddedModules(js);
  ASSERT_EQ(modules.size(), 1u);
  EXPECT_EQ(modules[0].bytes,
            makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}));
  EXPECT_EQ(js[modules[0].start], '[');
  EXPECT_EQ(js[modules[0].end - 1], ']');
}

TEST(JsEmbeddedModuleTest, VariousNumericFormatsAndTrailingComma) {
  std::string js = R"(
    var raw = [
      0, 97, 115, 109,
      0o1, 0b0000_0000, +1, -1, -128, 0Xff,
    ];
  )";

  auto modules = findEmbeddedModules(js);
  ASSERT_EQ(modules.size(), 1u);
  EXPECT_EQ(
    modules[0].bytes,
    makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x01, 0xff, 0x80, 0xff}));
}

TEST(JsEmbeddedModuleTest, InnerCommentsAndWhitespace) {
  std::string js = R"(
    const buf = [
      // Wasm magic
      0x00, 0x61, /* 'a' */ 0x73, 0x6d,
      /* version 1 */
      0x01, 0x00, 0x00, 0x00 // end of header
      /* trailing comment */
    ];
  )";

  auto modules = findEmbeddedModules(js);
  ASSERT_EQ(modules.size(), 1u);
  EXPECT_EQ(modules[0].bytes,
            makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}));
}

TEST(JsEmbeddedModuleTest, IgnoresStringsAndComments) {
  std::string js = R"(
    // [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x01]
    /*
      [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x02]
    */
    const s1 = "[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x03]";
    const s2 = 'escaped \' [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x04]';
    const s3 = `multiline template \`
      [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x05]
    `;
    const actual = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x06];
  )";

  auto modules = findEmbeddedModules(js);
  ASSERT_EQ(modules.size(), 1u);
  EXPECT_EQ(modules[0].bytes,
            makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x06}));
}

TEST(JsEmbeddedModuleTest, EarlyRejectionAndNestedArrays) {
  std::string js = R"(
    let empty = [];
    let tooShort = [0x00, 0x61, 0x73];
    let nonWasm = [1, 2, 3, 4, 5, 6, 7, 8];
    let wrongMagic = [0x00, 0x61, 0x73, 0x6e, 1, 0, 0, 1];
    let outOfRangePos = [0x00, 0x61, 0x73, 0x6d, 256, 0, 0, 2];
    let outOfRangeNeg = [0x00, 0x61, 0x73, 0x6d, -129, 0, 0, 3];
    let nonConstant = [0x00, 0x61, 0x73, 0x6d, foo(), 0, 0, 4];
    let nested = [[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x05]];
  )";

  auto modules = findEmbeddedModules(js);
  ASSERT_EQ(modules.size(), 1u);
  EXPECT_EQ(modules[0].bytes,
            makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x05}));
  EXPECT_EQ(js.substr(modules[0].start, modules[0].end - modules[0].start),
            "[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x05]");
}

TEST(JsEmbeddedModuleTest, MultiModuleReverseOrderSplicing) {
  std::string js = R"(
    const m0 = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0a]);
    const m1 = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0b]);
    const m2 = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0c]);
  )";

  auto modules = findEmbeddedModules(js);
  ASSERT_EQ(modules.size(), 3u);
  EXPECT_EQ(static_cast<uint8_t>(modules[0].bytes.back()), 0x0au);
  EXPECT_EQ(static_cast<uint8_t>(modules[1].bytes.back()), 0x0bu);
  EXPECT_EQ(static_cast<uint8_t>(modules[2].bytes.back()), 0x0cu);

  // Replace from last to first without rescanning, verifying earlier offsets do
  // not drift.
  auto reducedBytes =
    makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00});
  std::string formatted = formatByteArray(reducedBytes);

  for (size_t i = modules.size(); i > 0; --i) {
    const auto& mod = modules[i - 1];
    js.replace(mod.start, mod.end - mod.start, formatted);
  }

  auto rescanned = findEmbeddedModules(js);
  ASSERT_EQ(rescanned.size(), 3u);
  for (const auto& mod : rescanned) {
    EXPECT_EQ(mod.bytes, reducedBytes);
  }
}

TEST(JsEmbeddedModuleTest, FormatByteArray) {
  EXPECT_EQ(formatByteArray({}), "[]");

  auto bytes24 = makeBytes({0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
                            0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x03, 0x02,
                            0x01, 0x00, 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b});
  std::string expected24 =
    "[\n"
    "  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x04, 0x01, 0x60, "
    "0x00, 0x00, 0x03, 0x02,\n"
    "  0x01, 0x00, 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b\n"
    "]";
  EXPECT_EQ(formatByteArray(bytes24), expected24);

  // Round-trip formatted array back through findEmbeddedModules.
  auto parsed = findEmbeddedModules(expected24);
  ASSERT_EQ(parsed.size(), 1u);
  EXPECT_EQ(parsed[0].bytes, bytes24);
}