1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import type { RawSourceMap } from 'typescript'; 17import { SourceMap } from 'magic-string'; 18import { SourceMapSegment, decode } from '@jridgewell/sourcemap-codec'; 19import assert from 'assert'; 20 21enum SegmentIndex { 22 ORIGINAL_COLUMN_INDEX = 0, 23 SOURCE_INDEX = 1, 24 TRANSFORMED_LINE_INDEX = 2, 25 TRANSFORMED_COLUMN_INDEX = 3, 26 NAME_INDEX = 4, 27} 28 29/** 30 * The sourcemap format with decoded mappings with number type. 31 */ 32export interface ExistingDecodedSourceMap { 33 file?: string; 34 mappings: SourceMapSegment[][]; 35 names?: string[]; 36 sourceRoot?: string; 37 sources: string[]; 38 sourcesContent?: string[]; 39 version: number; 40} 41 42interface BaseSource { 43 traceSegment(line: number, column: number, name: string): SourceMapSegmentObj | null; 44} 45 46/** 47 * The source file info. 48 */ 49export class Source implements BaseSource { 50 readonly content: string | null; 51 readonly filename: string; 52 isOriginal = true; 53 54 constructor(filename: string, content: string | null) { 55 this.filename = filename; 56 this.content = content; 57 } 58 59 traceSegment(line: number, column: number, name: string): SourceMapSegmentObj { 60 return { column, line, name, source: this }; 61 } 62} 63 64/** 65 * The interpreted sourcemap line and column info. 66 */ 67export interface SourceMapSegmentObj { 68 column: number; 69 line: number; 70 name: string; 71 source: Source; 72} 73 74type MappingsNameType = { mappings: readonly SourceMapSegment[][]; names?: readonly string[] }; 75type TracedMappingsType = { mappings: SourceMapSegment[][]; names: string[]; sources: string[] }; 76 77/** 78 * Type of the map parameter of the SourceMapLink class. 79 */ 80export type MappingsNameTypeForTest = MappingsNameType; 81 82/** 83 * Provide api tools related to sourcemap. 84 */ 85export class SourceMapLink implements BaseSource { 86 readonly mappings: readonly SourceMapSegment[][]; 87 readonly names?: readonly string[]; 88 readonly sources: BaseSource[]; 89 90 constructor(map: MappingsNameType, sources: BaseSource[]) { 91 this.sources = sources; 92 this.names = map.names; 93 this.mappings = map.mappings; 94 } 95 96 traceMappings(): TracedMappingsType { 97 const tracedSources: string[] = []; 98 const sourceIndexMap = new Map<string, number>(); 99 const sourcesContent: (string | null)[] = []; 100 const tracednames: string[] = []; 101 const nameIndexMap = new Map<string, number>(); 102 103 const mappings = []; 104 105 for (const line of this.mappings) { 106 const tracedLine: SourceMapSegment[] = []; 107 108 for (const segment of line) { 109 if (segment.length === 1) { // The number of elements is insufficient. 110 continue; 111 } 112 const source = this.sources[segment[SegmentIndex.SOURCE_INDEX]]; 113 if (!source) { 114 continue; 115 } 116 // segment[2] records the line number of the code before transform, segment[3] records the column number of the code before transform. 117 // segment[4] records the name from the names array. 118 assert(segment.length >= 4, 'The length of the mapping segment is incorrect.'); 119 let line: number = segment[SegmentIndex.TRANSFORMED_LINE_INDEX]; 120 let column: number = segment[SegmentIndex.TRANSFORMED_COLUMN_INDEX]; 121 // If the length of the segment is 5, it will have name content. 122 let name: string = segment.length === 5 ? this.names[segment[SegmentIndex.NAME_INDEX]] : ''; 123 const traced = source.traceSegment(line, column, name); 124 125 if (traced) { 126 this.analyzeTracedSource(traced, tracedSources, sourceIndexMap, sourcesContent); 127 let sourceIndex = sourceIndexMap.get(traced.source.filename); 128 const targetSegment: SourceMapSegment = [segment[SegmentIndex.ORIGINAL_COLUMN_INDEX], sourceIndex, traced.line, traced.column]; 129 this.recordTracedName(traced, tracednames, nameIndexMap, targetSegment); 130 tracedLine.push(targetSegment); 131 } 132 } 133 134 mappings.push(tracedLine); 135 } 136 137 return { mappings, names: tracednames, sources: tracedSources }; 138 } 139 140 analyzeTracedSource(traced: SourceMapSegmentObj, tracedSources: string[], sourceIndexMap: Map<string, number>, sourcesContent: (string | null)[]): void { 141 const content = traced.source.content; 142 const filename = traced.source.filename; 143 // Get the source index from sourceIndexMap, which is the second element of sourcemap. 144 let sourceIndex = sourceIndexMap.get(filename); 145 if (sourceIndex === undefined) { 146 sourceIndex = tracedSources.length; 147 tracedSources.push(filename); 148 sourceIndexMap.set(filename, sourceIndex); 149 sourcesContent[sourceIndex] = content; 150 } else if (sourcesContent[sourceIndex] == null) { // Update text when content is empty. 151 sourcesContent[sourceIndex] = content; 152 } else if (content != null && sourcesContent[sourceIndex] !== content) { 153 throw new Error(`Multiple conflicting contents for sourcemap source: ${filename}`); 154 } 155 } 156 157 recordTracedName(traced: SourceMapSegmentObj, tracednames: string[], nameIndexMap: Map<string, number>, targetSegment: SourceMapSegment): void { 158 if (traced.name) { 159 const name = traced.name; 160 let nameIndex = nameIndexMap.get(name); 161 if (nameIndex === undefined) { 162 nameIndex = tracednames.length; 163 tracednames.push(name); 164 nameIndexMap.set(name, nameIndex); 165 } 166 // Add the fourth element: name position 167 targetSegment.push(nameIndex); 168 } 169 } 170 171 traceSegment(line: number, column: number, name: string): SourceMapSegmentObj | null { 172 const segments = this.mappings[line]; 173 if (!segments) { 174 return null; 175 } 176 177 // Binary search segment for the target columns. 178 let binarySearchStart = 0; 179 let binarySearchEnd = segments.length - 1; // Get the last elemnt index. 180 181 while (binarySearchStart <= binarySearchEnd) { 182 // Calculate the intermediate index. 183 const m = (binarySearchStart + binarySearchEnd) >> 1; 184 const tempSegment = segments[m]; 185 let tempColumn = tempSegment[SegmentIndex.ORIGINAL_COLUMN_INDEX]; 186 // If a sourcemap does not have sufficient resolution to contain a necessary mapping, e.g. because it only contains line information, we 187 // use the best approximation we could find 188 if (tempColumn === column || binarySearchStart === binarySearchEnd) { 189 if (tempSegment.length === 1) { // The number of elements is insufficient. 190 return null; 191 } 192 const tracedSource = tempSegment[SegmentIndex.SOURCE_INDEX]; 193 const source = this.sources[tracedSource]; 194 if (!source) { 195 return null; 196 } 197 198 let tracedLine: number = tempSegment[SegmentIndex.TRANSFORMED_LINE_INDEX]; 199 let tracedColumn: number = tempSegment[SegmentIndex.TRANSFORMED_COLUMN_INDEX]; 200 let tracedName: string = tempSegment.length === 5 ? this.names[tempSegment[SegmentIndex.NAME_INDEX]] : name; 201 return source.traceSegment(tracedLine, tracedColumn, tracedName); 202 } 203 if (tempColumn > column) { 204 // Target is in the left half 205 binarySearchEnd = m - 1; 206 } else { 207 // Target is in the right half 208 binarySearchStart = m + 1; 209 } 210 } 211 212 return null; 213 } 214} 215 216/** 217 * Decode the sourcemap from string format to number format. 218 * @param map The sourcemap with raw string format, eg. mappings: IAGS,OAAO,GAAE,MAAM,CAAA; 219 * @returns The sourcemap with decoded number format, eg. mappings: [4,0,3,9], [7,0,0,7], [3,0,0,2], [6,0,0,6], [1,0,0,0] 220 */ 221export function decodeSourcemap(map: RawSourceMap): ExistingDecodedSourceMap | null { 222 if (!map) { 223 return null; 224 } 225 if (map.mappings === '') { 226 return { mappings: [], names: [], sources: [], version: 3 }; // 3 is the sourcemap version. 227 } 228 const mappings: SourceMapSegment[][] = decode(map.mappings); 229 return { ...map, mappings: mappings }; 230} 231 232function generateChain(sourcemapChain: ExistingDecodedSourceMap[], map: RawSourceMap): void { 233 sourcemapChain.push(decodeSourcemap(map)); 234} 235 236/** 237 * Merge the sourcemaps of the two processes into the sourcemap of the complete process. 238 * @param previousMap The sourcemap before obfuscation process, such as ets-loader transform 239 * @param currentMap The sourcemap of obfuscation process 240 * @returns The merged sourcemap 241 */ 242export function mergeSourceMap(previousMap: RawSourceMap, currentMap: RawSourceMap): RawSourceMap { 243 const sourcemapChain: ExistingDecodedSourceMap[] = []; 244 // The ets-loader esmodule mode processes one file at a time, so get the file name at index 1 245 const sourceFileName = previousMap.sources.length === 1 ? previousMap.sources[0] : ''; 246 const source: Source = new Source(sourceFileName, null); 247 generateChain(sourcemapChain, previousMap); 248 generateChain(sourcemapChain, currentMap); 249 const collapsedSourcemap: SourceMapLink = sourcemapChain.reduce( 250 (source: BaseSource, map: ExistingDecodedSourceMap): SourceMapLink => { 251 return new SourceMapLink(map, [source]); 252 }, 253 source, 254 ) as SourceMapLink; 255 const tracedMappings: TracedMappingsType = collapsedSourcemap.traceMappings(); 256 const result: RawSourceMap = new SourceMap({ ...tracedMappings, file: previousMap.file }) as RawSourceMap; 257 result.sourceRoot = previousMap.sourceRoot; 258 return result; 259} 260