diff --git a/src/js_native_api_v8.cpp b/src/js_native_api_v8.cpp index 5e8783fca32c25a6eb2791fe19c0f1cabcae6e73..c87e95d05ccb13c1a4dd16858a5ba1e3c4819064 100644 --- a/src/js_native_api_v8.cpp +++ b/src/js_native_api_v8.cpp @@ -37,7 +37,7 @@ #include "libplatform/libplatform.h" #include "libplatform/v8-tracing.h" #include "platform/platform.h" -#include "sourcemap.def" +#include "sourcemap.h" #ifdef V8_USE_PERFETTO #error Unsupported Perfetto. @@ -1288,10 +1288,7 @@ v8::MaybeLocal PrepareStackTraceCallback(v8::Local conte buffer << sourceMapfile.rdbuf(); content = buffer.str(); } - auto sourceMapObject = - v8::String::NewFromUtf8(isolate, content.c_str(), v8::NewStringType::kNormal, content.length()); - v8::Local args[] = { error, trace, sourceMapObject.ToLocalChecked() }; - return resultFunc->Call(moduleContext, v8::Undefined(isolate), jsvm::ArraySize(args), args); + return ParseSourceMap(isolate, moduleContext, error, trace, resultFunc, content); } JSVM_Status OH_JSVM_CompileScriptWithOrigin(JSVM_Env env, diff --git a/src/jsvm_util.h b/src/jsvm_util.h index 03b80c287e90d5cea16567e2206696c7343031a0..598aaaa682c051cdacb20fd96f98ae99eeddbea8 100644 --- a/src/jsvm_util.h +++ b/src/jsvm_util.h @@ -47,15 +47,11 @@ #include "v8-profiler.h" #include "v8.h" -// Use FORCE_INLINE on functions that have a debug-category-enabled check first -// and then ideally only a single function call following it, to maintain -// performance for the common case (no debugging used). + #ifdef __GNUC__ #define FORCE_INLINE __attribute__((always_inline)) -#define COLD_NOINLINE __attribute__((cold, noinline)) #else #define FORCE_INLINE -#define COLD_NOINLINE #endif namespace jsvm { diff --git a/src/sourcemap.def b/src/sourcemap.def deleted file mode 100644 index a959041a6988a1514b80beac1f5411483c6701e1..0000000000000000000000000000000000000000 --- a/src/sourcemap.def +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2024 Huawei Device Co., Ltd. - * 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. - */ - -static std::string SourceMapRunner = R"JS( -let base64Map; -const VLQ_BASE_SHIFT = 5; -const VLQ_BASE_MASK = (1 << 5) - 1; -const VLQ_CONTINUATION_MASK = 1 << 5; -class StringCharIterator { - constructor(string) { - this._string = string; - this._position = 0 - } - next() { - return this._string.charAt(this._position++) - } - peek() { - return this._string.charAt(this._position) - } - hasNext() { - return this._position < this._string.length - } -} -class SourceMap { - #payload; - #mappings = []; - #sources = {}; - #sourceContentByURL = {}; - constructor(payload) { - if (!base64Map) { - const base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - base64Map = {}; - for (let i = 0; i < base64Digits.length; ++i) base64Map[base64Digits[i]] = i - } - this.#payload = cloneSourceMapV3(payload); - this.#parseMappingPayload() - } - get payload() { - return cloneSourceMapV3(this.#payload) - } - #parseMappingPayload = () => { - if (this.#payload.sections) { - this.#parseSections(this.#payload.sections) - } else { - this.#parseMap(this.#payload, 0, 0) - } - this.#mappings.sort(compareSourceMapEntry) - }; - #parseSections = (sections) => { - for (let i = 0; i < sections.length; ++i) { - const section = sections[i]; - this.#parseMap(section.map, section.offset.line, section.offset.column) - } - }; - findEntry(lineOffset, columnOffset) { - let first = 0; - let count = this.#mappings.length; - while (count > 1) { - const step = count >> 1; - const middle = first + step; - const mapping = this.#mappings[middle]; - if (lineOffset < mapping[0] || (lineOffset === mapping[0] && columnOffset < mapping[1])) { - count = step - } else { - first = middle; - count -= step - } - } - const entry = this.#mappings[first]; - if (!first && entry && (lineOffset < entry[0] || (lineOffset === entry[0] && columnOffset < entry[1]))) { - return {} - } else if (!entry) { - return {} - } - return { - generatedLine: entry[0], - generatedColumn: entry[1], - originalSource: entry[2], - originalLine: entry[3], - originalColumn: entry[4], - name: entry[5], - } - } - findOrigin(lineNumber, columnNumber) { - const range = this.findEntry(lineNumber - 1, columnNumber - 1); - if (range.originalSource === undefined || range.originalLine === undefined || range.originalColumn === undefined || range.generatedLine === undefined || range.generatedColumn === undefined) { - return {} - } - const lineOffset = lineNumber - range.generatedLine; - const columnOffset = columnNumber - range.generatedColumn; - return { - name: range.name, - fileName: range.originalSource, - lineNumber: range.originalLine + lineOffset, - columnNumber: range.originalColumn + columnOffset, - } - } - #parseMap(map, lineNumber, columnNumber) { - let sourceIndex = 0; - let sourceLineNumber = 0; - let sourceColumnNumber = 0; - let nameIndex = 0; - const sources = []; - const originalToCanonicalURLMap = {}; - for (let i = 0; i < map.sources.length; ++i) { - const url = map.sources[i]; - originalToCanonicalURLMap[url] = url; - sources.push(url); - this.#sources[url] = true; - if (map.sourcesContent && map.sourcesContent[i]) this.#sourceContentByURL[url] = map.sourcesContent[i] - } - const stringCharIterator = new StringCharIterator(map.mappings); - let sourceURL = sources[sourceIndex]; - while (true) { - if (stringCharIterator.peek() === ',') stringCharIterator.next(); - else { - while (stringCharIterator.peek() === ';') { - lineNumber += 1; - columnNumber = 0; - stringCharIterator.next() - } - if (!stringCharIterator.hasNext()) break - } - columnNumber += decodeVLQ(stringCharIterator); - if (isSeparator(stringCharIterator.peek())) { - this.#mappings.push([lineNumber, columnNumber]); - continue - } - const sourceIndexDelta = decodeVLQ(stringCharIterator); - if (sourceIndexDelta) { - sourceIndex += sourceIndexDelta; - sourceURL = sources[sourceIndex] - } - sourceLineNumber += decodeVLQ(stringCharIterator); - sourceColumnNumber += decodeVLQ(stringCharIterator); - let name; - if (!isSeparator(stringCharIterator.peek())) { - nameIndex += decodeVLQ(stringCharIterator); - name = map.names?.[nameIndex] - } - this.#mappings.push([lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber, name], ) - } - } -} - -function isSeparator(a) { - return a === ',' || a === ';' -} - -function decodeVLQ(a) { - let result = 0; - let shift = 0; - let digit; - do { - digit = base64Map[a.next()]; - result += (digit & VLQ_BASE_MASK) << shift; - shift += VLQ_BASE_SHIFT - } while (digit & VLQ_CONTINUATION_MASK); - const negative = result & 1; - result >>>= 1; - if (!negative) { - return result - } - return -result | (1 << 31) -} - -function cloneSourceMapV3(a) { - a = { - ...payload - }; - for (const key in a) { - if (Object.prototype.hasOwnProperty.call(a, key) && Array.isArray(a[key])) { - a[key] = a[key].slice() - } - } - return a -} - -function compareSourceMapEntry(a, b) { - const { - 0: lineNumber1, - 1: columnNumber1 - } = a; - const { - 0: lineNumber2, - 1: columnNumber2 - } = b; - if (lineNumber1 !== lineNumber2) { - return lineNumber1 - lineNumber2 - } - return columnNumber1 - columnNumber2 -} -result = function(a, b, c) { - try { - const payload = JSON.parse(c) const sm = new SourceMap(payload) const preparedStack = b.map((t, i) => { - const str = i !== 0 ? '\n at ' : ''; - const { - originalLine, - originalColumn, - originalSource, - } = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1); - if (originalSource && originalLine !== undefined && originalColumn !== undefined) { - let fileName = t.getFileName(); - if (fileName === undefined) { - fileName = t.getEvalOrigin() - } - const fnName = t.getFunctionName() ?? t.getMethodName(); - const typeName = t.getTypeName(); - const namePrefix = typeName !== null && typeName !== 'global' ? `${typeName}.` : ''; - const originalName = `${namePrefix}${fnName||''}`; - const hasName = !!originalName; - return `${str}${originalName}${hasName?' (':''}` + `${originalSource}:${originalLine+1}:` + `${originalColumn+1}${hasName?')':''}` - } - return `${str}${t}` - }).join(''); - return `${a}\n at ${preparedStack}` - } catch (e) { - const originStack = b.map((t, i) => { - const str = i !== 0 ? '\n at ' : ''; - return `${str}${t}` - }).join('') return `${a}\n at ${originStack}` - } -} -)JS"; diff --git a/src/sourcemap.h b/src/sourcemap.h new file mode 100644 index 0000000000000000000000000000000000000000..aac06aec5b61b7cb0fd5033ce0f56d3b74246cb8 --- /dev/null +++ b/src/sourcemap.h @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * 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. + */ + +#ifndef SOURCE_MAP_H +#define SOURCE_MAP_H + +#include +#include +#include +#include +#include +#include + +#include "jsvm_util.h" + +// Base64 VLQ decoding +int DecodeVLQ(const std::string& input, size_t& pos) +{ + // Const value + constexpr int vlqBaseShift = 5; + constexpr int vlqBaseMask = (1 << 5) - 1; + constexpr int vlqContinuationMask = 1 << 5; + static std::unordered_map base64Map { + { 'A', 0 }, { 'B', 1 }, { 'C', 2 }, { 'D', 3 }, { 'E', 4 }, { 'F', 5 }, { 'G', 6 }, { 'H', 7 }, + { 'I', 8 }, { 'J', 9 }, { 'K', 10 }, { 'L', 11 }, { 'M', 12 }, { 'N', 13 }, { 'O', 14 }, { 'P', 15 }, + { 'Q', 16 }, { 'R', 17 }, { 'S', 18 }, { 'T', 19 }, { 'U', 20 }, { 'V', 21 }, { 'W', 22 }, { 'X', 23 }, + { 'Y', 24 }, { 'Z', 25 }, { 'a', 26 }, { 'b', 27 }, { 'c', 28 }, { 'd', 29 }, { 'e', 30 }, { 'f', 31 }, + { 'g', 32 }, { 'h', 33 }, { 'i', 34 }, { 'j', 35 }, { 'k', 36 }, { 'l', 37 }, { 'm', 38 }, { 'n', 39 }, + { 'o', 40 }, { 'p', 41 }, { 'q', 42 }, { 'r', 43 }, { 's', 44 }, { 't', 45 }, { 'u', 46 }, { 'v', 47 }, + { 'w', 48 }, { 'x', 49 }, { 'y', 50 }, { 'z', 51 }, { '0', 52 }, { '1', 53 }, { '2', 54 }, { '3', 55 }, + { '4', 56 }, { '5', 57 }, { '6', 58 }, { '7', 59 }, { '8', 60 }, { '9', 61 }, { '+', 62 }, { '/', 63 } + }; + + // Decode VLQ + int result = 0; + int shift = 0; + int digit; + do { + digit = base64Map[input[pos++]]; + result += (digit & vlqBaseMask) << shift; + shift += vlqBaseShift; + } while (digit & vlqContinuationMask); + + // Fix the sign + int negative = result & 1; + result >>= 1; + return negative ? -result : result; +} + +struct Offset { +public: + constexpr Offset(int line, int column) : line(line), column(column) {} + + constexpr static Offset InvalidOffset() + { + return Offset(-1, -1); + } + + bool IsInvalid() const + { + return line < 0 || column < 0; + } + + bool operator<(const Offset& other) const + { + return line < other.line || (line == other.line && column < other.column); + } + +public: + int line; + int column; +}; + +struct Mappings { +public: + explicit Mappings(Offset traceOffset, + Offset sourceOffset = Offset::InvalidOffset(), + std::string sourceName = "", + int nameIdx = -1) + : traceOffset(traceOffset), sourceOffset(sourceOffset), nameIdx(nameIdx), sourceName(sourceName) + {} + + bool IsInvalid() const + { + return traceOffset.IsInvalid() || sourceOffset.IsInvalid(); + } + + bool operator<(const Mappings& other) const + { + return traceOffset < other.traceOffset; + } + + std::string ToString() const + { + if (IsInvalid()) { + return ""; + } + return sourceName + ":" + std::to_string(sourceOffset.line + 1) + ":" + std::to_string(sourceOffset.column + 1); + } + +public: + Offset traceOffset; + Offset sourceOffset; + int nameIdx; + std::string sourceName; +}; + +class SourceMap { +public: + SourceMap(v8::Isolate* isolate, v8::Local context, v8::Local payload) + : isolate(isolate), context(context), payload(payload) + { + ParseMappingPayload(); + } + + Mappings FindEntry(int lineOffset, int columnOffset) + { + int count = sourceMappings.size(); + if (count == 0) { + return Mappings(Offset::InvalidOffset()); + } + + int left = 0; + Offset offset(lineOffset, columnOffset); + + while (count > 1) { + int step = count >> 1; + int middle = left + step; + if (offset < sourceMappings[middle].traceOffset) { + count = step; + } else { + left = middle; + count -= step; + } + } + + if (left == 0 && offset < sourceMappings[0].traceOffset) { + return Mappings(Offset::InvalidOffset()); + } + + return sourceMappings[left]; + } + +private: + void ParseMappingPayload(); + void ParseMap(v8::Local map, int line, int column); + void ParseMappings(const std::string& mappings, + const std::vector& sources, + int lineNumber, + int columnNumber); + void ParseSections(v8::Local sections); + std::vector ParseSourceNames(v8::Local sources); + +private: + v8::Isolate* isolate; + v8::Local context; + v8::Local payload; + std::vector sourceMappings; +}; + +void SourceMap::ParseMappingPayload() +{ + auto sections = payload->Get(context, v8::String::NewFromUtf8Literal(isolate, "sections")); + if (!sections.IsEmpty() && sections.ToLocalChecked()->ToBoolean(isolate)->Value()) { + ParseSections(sections.ToLocalChecked()); + } else { + ParseMap(payload, 0, 0); + } + + std::sort(sourceMappings.begin(), sourceMappings.end()); +} + +std::vector SourceMap::ParseSourceNames(v8::Local sources) +{ + std::vector names; + + for (uint32_t i = 0; i < sources->Length(); ++i) { + auto element = sources->Get(context, i); + // should be string + v8::Local fromMaybe; + if (!element.ToLocal(&fromMaybe) || !fromMaybe->IsString()) { + names.emplace_back(""); + continue; + } + v8::String::Utf8Value source(isolate, fromMaybe); + names.emplace_back(*source); + } + + return names; +} + +void SourceMap::ParseMap(v8::Local map, int line, int column) +{ + if (!map->IsObject()) { + return; + } + + auto mapObj = map.As(); + + // Get sources + auto maybeSources = mapObj->Get(context, v8::String::NewFromUtf8Literal(isolate, "sources")); + v8::Local fromMaybe; + if (!maybeSources.ToLocal(&fromMaybe) || !fromMaybe->IsArray()) { + return; + } + auto arr = fromMaybe.As(); + auto names = ParseSourceNames(arr); + + // Get mappings + v8::Local mappingsValue = + mapObj->Get(context, v8::String::NewFromUtf8Literal(isolate, "mappings")).ToLocalChecked(); + if (!mappingsValue->IsString()) { + return; + } + v8::String::Utf8Value mappingsStr(isolate, mappingsValue); + + // Parse mappings + ParseMappings(*mappingsStr, names, line, column); +} + +void SourceMap::ParseMappings(const std::string& mappings, + const std::vector& sources, + int lineNumber, + int columnNumber) +{ + size_t pos = 0; + int sourceIndex = 0; + int sourceLineNumber = 0; + int sourceColumnNumber = 0; + int nameIndex = 0; + + while (pos < mappings.length()) { + if (mappings[pos] == ',') { + pos++; + } else { + while (pos < mappings.length() && mappings[pos] == ';') { + lineNumber++; + columnNumber = 0; + pos++; + } + + if (pos == mappings.length()) { + break; + } + } + + columnNumber += DecodeVLQ(mappings, pos); + if (pos >= mappings.length() || mappings[pos] == ',' || mappings[pos] == ';') { + sourceMappings.emplace_back(Offset(lineNumber, columnNumber)); + continue; + } + + int sourceIndexDelta = DecodeVLQ(mappings, pos); + if (sourceIndexDelta) { + sourceIndex += sourceIndexDelta; + } + sourceLineNumber += DecodeVLQ(mappings, pos); + sourceColumnNumber += DecodeVLQ(mappings, pos); + + if (pos < mappings.length() && mappings[pos] != ',' && mappings[pos] != ';') { + nameIndex += DecodeVLQ(mappings, pos); + } + sourceMappings.emplace_back(Offset(lineNumber, columnNumber), Offset(sourceLineNumber, sourceColumnNumber), + sources[sourceIndex], nameIndex); + } +} + +void SourceMap::ParseSections(v8::Local sections) +{ + if (!sections->IsArray()) { + return; + } + + v8::Local arr = sections.As(); + for (uint32_t i = 0; i < arr->Length(); ++i) { + auto element = arr->Get(context, i); + + v8::Local fromMaybe; + if (!element.ToLocal(&fromMaybe) || !fromMaybe->IsObject()) { + continue; + } + auto section = fromMaybe.As(); + + auto maybeMap = section->Get(context, v8::String::NewFromUtf8Literal(isolate, "map")); + if (maybeMap.IsEmpty()) { + continue; + } + + auto maybeOffset = section->Get(context, v8::String::NewFromUtf8Literal(isolate, "offset")); + if (!maybeOffset.ToLocal(&fromMaybe) || !fromMaybe->IsObject()) { + continue; + } + auto offset = fromMaybe.As(); + + auto maybeLine = offset->Get(context, v8::String::NewFromUtf8Literal(isolate, "line")); + if (!maybeLine.ToLocal(&fromMaybe)) { + continue; + } + auto maybeInt = fromMaybe->ToInt32(context); + int line = maybeInt.IsEmpty() ? -1 : maybeInt.ToLocalChecked()->Value(); + + auto maybeColumn = offset->Get(context, v8::String::NewFromUtf8Literal(isolate, "column")); + if (!maybeColumn.ToLocal(&fromMaybe)) { + continue; + } + maybeInt = fromMaybe->ToInt32(context); + int column = maybeInt.IsEmpty() ? -1 : maybeInt.ToLocalChecked()->Value(); + if (line == -1 || column == -1) { + continue; + } + + ParseMap(maybeMap.ToLocalChecked(), line, column); + } +} + +v8::MaybeLocal HandleError(v8::Isolate* isolate, + v8::Local ctx, + v8::Local error, + v8::Local trace) +{ + v8::Local stackStr; + if (!error->ToString(ctx).ToLocal(&stackStr)) { + return v8::MaybeLocal(); + } + + auto left = v8::String::NewFromUtf8Literal(isolate, "\n at "); + + for (uint32_t i = 0; i < trace->Length(); ++i) { + v8::Local element = trace->Get(ctx, i).ToLocalChecked(); + v8::Local str; + + if (!element->ToString(ctx).ToLocal(&str)) { + return v8::MaybeLocal(); + } + + auto traceStr = v8::String::Concat(isolate, left, str); + + stackStr = v8::String::Concat(isolate, stackStr, traceStr); + } + + return stackStr; +} + +static std::string SourceMapRunner = R"JS( +result = (t, originalSouceInfo) => { + const str = '\n at '; + try { + if (originalSouceInfo != "") { + let fileName = t.getFileName(); + if (fileName === undefined) { + fileName = t.getEvalOrigin() + } + const fnName = t.getFunctionName() ?? t.getMethodName(); + const typeName = t.getTypeName(); + const namePrefix = typeName !== null && typeName !== 'global' ? `${typeName}.` : ''; + const originalName = `${namePrefix}${fnName||''}`; + const hasName = !!originalName; + return `${str}${originalName}${hasName?' (':''}` + originalSouceInfo + `${hasName?')':''}` + } + return `${str}${t}` + } catch (e) { + return `${str}${t}` + } +} +)JS"; + +int GetAndCallFunction(v8::Isolate* isolate, + v8::Local ctx, + v8::Local obj, + v8::Local funcName) +{ + auto maybeFunc = obj->Get(ctx, funcName); + + v8::Local value; + if (!maybeFunc.ToLocal(&value) || !value->IsFunction()) { + return -1; + } + + auto func = value.As(); + + // eval obj.funcName() + auto maybeRes = func->Call(ctx, obj, 0, nullptr); + if (maybeRes.IsEmpty()) { + return -1; + } + + auto maybeInt = maybeRes.ToLocalChecked()->ToInt32(ctx); + return maybeInt.IsEmpty() ? -1 : maybeInt.ToLocalChecked()->Value(); +} + +v8::MaybeLocal ParseSourceMap(v8::Isolate* isolate, + v8::Local ctx, + v8::Local error, + v8::Local trace, + v8::Local toStringFunc, + std::string& sourceMapContent) +{ + v8::TryCatch tryCatch(isolate); + auto sourceMapStr = v8::String::NewFromUtf8(isolate, sourceMapContent.c_str(), v8::NewStringType::kNormal, + sourceMapContent.length()) + .ToLocalChecked(); + + // Parse json string to object + v8::Local sourceMapObj; + if (!v8::JSON::Parse(ctx, sourceMapStr).ToLocal(&sourceMapObj) || !sourceMapObj->IsObject()) { + return HandleError(isolate, ctx, error, trace); + } + + v8::Local stackStr; + if (!error->ToString(ctx).ToLocal(&stackStr)) { + return v8::MaybeLocal(); + } + + SourceMap sourceMap(isolate, ctx, sourceMapObj.As()); + + // Get line and column + auto getLineStr = v8::String::NewFromUtf8Literal(isolate, "getLineNumber"); + auto getColumnStr = v8::String::NewFromUtf8Literal(isolate, "getColumnNumber"); + + for (uint32_t i = 0; i < trace->Length(); ++i) { + v8::Local element = trace->Get(ctx, i).ToLocalChecked(); + if (!element->IsObject()) { + continue; + } + + auto t = element.As(); + auto line = GetAndCallFunction(isolate, ctx, t, getLineStr) - 1; + auto column = GetAndCallFunction(isolate, ctx, t, getColumnStr) - 1; + + auto str = sourceMap.FindEntry(line, column).ToString(); + auto originalSouceInfo = + v8::String::NewFromUtf8(isolate, str.c_str(), v8::NewStringType::kNormal, str.length()).ToLocalChecked(); + + v8::Local args[] = { t, originalSouceInfo }; + auto traceStr = toStringFunc->Call(ctx, v8::Undefined(isolate), jsvm::ArraySize(args), args) + .ToLocalChecked() + .As(); + stackStr = v8::String::Concat(isolate, stackStr, traceStr); + } + + // Check execption + if (tryCatch.HasCaught()) { + return HandleError(isolate, ctx, error, trace); + } + + return stackStr; +} + +#endif \ No newline at end of file