| /* | |
| * 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 "support/js-embedded-module.h" | |
| #include <cctype> | |
| #include <cstdint> | |
| namespace wasm { | |
| namespace { | |
| // Advances `pos` past a `//` single-line comment (leaving `pos` at the newline | |
| // character or end of `js`). Assumes `js.substr(pos, 2) == "//"`. | |
| void skipSingleLineComment(std::string_view js, size_t& pos) { | |
| pos += 2; | |
| while (pos < js.size() && js[pos] != '\n') { | |
| ++pos; | |
| } | |
| } | |
| // Advances `pos` past a `/* ... */` multi-line comment (or to the end of `js` | |
| // if the comment is unterminated). Assumes `js.substr(pos, 2) == "/*"`. | |
| void skipMultiLineComment(std::string_view js, size_t& pos) { | |
| pos += 2; | |
| while (pos + 1 < js.size() && !(js[pos] == '*' && js[pos + 1] == '/')) { | |
| ++pos; | |
| } | |
| if (pos + 1 < js.size()) { | |
| pos += 2; | |
| } else { | |
| pos = js.size(); | |
| } | |
| } | |
| // Advances `pos` past any contiguous ASCII whitespace, single-line comments, | |
| // and multi-line comments. | |
| void skipWhitespaceAndComments(std::string_view js, size_t& pos) { | |
| while (pos < js.size()) { | |
| if (std::isspace(static_cast<unsigned char>(js[pos]))) { | |
| ++pos; | |
| } else if (pos + 1 < js.size() && js[pos] == '/' && js[pos + 1] == '/') { | |
| skipSingleLineComment(js, pos); | |
| } else if (pos + 1 < js.size() && js[pos] == '/' && js[pos + 1] == '*') { | |
| skipMultiLineComment(js, pos); | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| // Parses a single integer literal representing an 8-bit byte at `pos` (in | |
| // decimal, `0x` hex, `0o` octal, or `0b` binary, with optional `+`/`-` sign and | |
| // `_` numeric separators). Accepts unsigned values in [0, 255] and signed | |
| // negative values in [-128, -1]. On success, advances `pos` past the literal, | |
| // writes the byte to `outByte`, and returns true. | |
| bool parseByteLiteral(std::string_view js, size_t& pos, char& outByte) { | |
| if (pos >= js.size()) { | |
| return false; | |
| } | |
| bool negative = false; | |
| if (js[pos] == '-' || js[pos] == '+') { | |
| negative = (js[pos] == '-'); | |
| ++pos; | |
| if (pos >= js.size()) { | |
| return false; | |
| } | |
| } | |
| int base = 10; | |
| if (js[pos] == '0' && pos + 1 < js.size()) { | |
| char next = js[pos + 1]; | |
| if (next == 'x' || next == 'X') { | |
| base = 16; | |
| pos += 2; | |
| } else if (next == 'o' || next == 'O') { | |
| base = 8; | |
| pos += 2; | |
| } else if (next == 'b' || next == 'B') { | |
| base = 2; | |
| pos += 2; | |
| } | |
| } | |
| uint32_t value = 0; | |
| bool hasDigits = false; | |
| while (pos < js.size()) { | |
| char c = js[pos]; | |
| if (c == '_') { | |
| ++pos; | |
| continue; | |
| } | |
| int digit = -1; | |
| if (c >= '0' && c <= '9') { | |
| digit = c - '0'; | |
| } else if (c >= 'a' && c <= 'f') { | |
| digit = 10 + (c - 'a'); | |
| } else if (c >= 'A' && c <= 'F') { | |
| digit = 10 + (c - 'A'); | |
| } else { | |
| break; | |
| } | |
| if (digit >= base) { | |
| return false; | |
| } | |
| hasDigits = true; | |
| value = value * base + digit; | |
| if ((!negative && value > 255) || (negative && value > 128)) { | |
| return false; | |
| } | |
| ++pos; | |
| } | |
| if (!hasDigits) { | |
| return false; | |
| } | |
| // Ensure the literal is not immediately followed by an identifier character. | |
| if (pos < js.size() && | |
| (std::isalnum(static_cast<unsigned char>(js[pos])) || js[pos] == '_')) { | |
| return false; | |
| } | |
| if (negative) { | |
| outByte = static_cast<char>(static_cast<uint8_t>(-static_cast<int>(value))); | |
| } else { | |
| outByte = static_cast<char>(static_cast<uint8_t>(value)); | |
| } | |
| return true; | |
| } | |
| // Attempts to parse a JavaScript array literal starting at index `start` | |
| // (`js[start] == '['`) as an embedded WebAssembly module. Rejects early if the | |
| // first 4 elements do not match the `\0asm` magic bytes (`0x00, 0x61, 0x73, | |
| // 0x6d`) or if any element is not a valid byte literal. | |
| bool tryParseWasmByteArray(std::string_view js, | |
| size_t start, | |
| EmbeddedModule& outModule) { | |
| static constexpr uint8_t WasmMagic[4] = {0x00, 0x61, 0x73, 0x6d}; | |
| size_t pos = start + 1; | |
| std::vector<char> bytes; | |
| while (true) { | |
| skipWhitespaceAndComments(js, pos); | |
| if (pos >= js.size()) { | |
| return false; | |
| } | |
| if (js[pos] == ']') { | |
| if (bytes.size() < 4) { | |
| return false; | |
| } | |
| outModule = EmbeddedModule{start, pos + 1, std::move(bytes)}; | |
| return true; | |
| } | |
| char byteVal = 0; | |
| if (!parseByteLiteral(js, pos, byteVal)) { | |
| return false; | |
| } | |
| // Early rejection if the first 4 bytes do not match \0asm. | |
| if (bytes.size() < 4 && | |
| static_cast<uint8_t>(byteVal) != WasmMagic[bytes.size()]) { | |
| return false; | |
| } | |
| bytes.push_back(byteVal); | |
| skipWhitespaceAndComments(js, pos); | |
| if (pos >= js.size()) { | |
| return false; | |
| } | |
| if (js[pos] == ',') { | |
| ++pos; | |
| } else if (js[pos] != ']') { | |
| return false; | |
| } | |
| } | |
| } | |
| } // namespace | |
| std::vector<EmbeddedModule> findEmbeddedModules(std::string_view js) { | |
| std::vector<EmbeddedModule> modules; | |
| size_t pos = 0; | |
| while (pos < js.size()) { | |
| char c = js[pos]; | |
| // Skip single-line and multi-line comments. | |
| if (c == '/' && pos + 1 < js.size()) { | |
| if (js[pos + 1] == '/') { | |
| skipSingleLineComment(js, pos); | |
| continue; | |
| } | |
| if (js[pos + 1] == '*') { | |
| skipMultiLineComment(js, pos); | |
| continue; | |
| } | |
| } | |
| // Skip string and template literals. | |
| if (c == '\'' || c == '"' || c == '`') { | |
| char quote = c; | |
| ++pos; | |
| while (pos < js.size()) { | |
| if (js[pos] == '\\') { | |
| pos += 2; | |
| continue; | |
| } | |
| if (js[pos] == quote) { | |
| ++pos; | |
| break; | |
| } | |
| if (quote != '`' && (js[pos] == '\n' || js[pos] == '\r')) { | |
| // Single- and double-quoted JS strings cannot span unescaped lines. | |
| ++pos; | |
| break; | |
| } | |
| ++pos; | |
| } | |
| continue; | |
| } | |
| if (c == '[') { | |
| EmbeddedModule mod; | |
| if (tryParseWasmByteArray(js, pos, mod)) { | |
| pos = mod.end; | |
| modules.push_back(std::move(mod)); | |
| continue; | |
| } | |
| } | |
| ++pos; | |
| } | |
| return modules; | |
| } | |
| std::string formatByteArray(const std::vector<char>& bytes) { | |
| if (bytes.empty()) { | |
| return "[]"; | |
| } | |
| static constexpr char HexDigits[] = "0123456789abcdef"; | |
| std::string out = "[\n"; | |
| // Each byte takes ~6 chars ("0x00, "), plus indentation and brackets. | |
| out.reserve(bytes.size() * 6 + (bytes.size() / 16 + 1) * 4 + 4); | |
| for (size_t i = 0; i < bytes.size(); ++i) { | |
| if (i % 16 == 0) { | |
| out += " "; | |
| } | |
| uint8_t b = static_cast<uint8_t>(bytes[i]); | |
| out += "0x"; | |
| out += HexDigits[b >> 4]; | |
| out += HexDigits[b & 0x0f]; | |
| if (i + 1 < bytes.size()) { | |
| if ((i + 1) % 16 == 0) { | |
| out += ",\n"; | |
| } else { | |
| out += ", "; | |
| } | |
| } else { | |
| out += "\n]"; | |
| } | |
| } | |
| return out; | |
| } | |
| } // namespace wasm |