| /* |
| * 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 |