• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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