|  | // Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file | 
|  | // for details. All rights reserved. Use of this source code is governed by a | 
|  | // BSD-style license that can be found in the LICENSE file. | 
|  |  | 
|  | package com.android.tools.r8.kotlin; | 
|  |  | 
|  | import com.android.tools.r8.naming.Range; | 
|  | import com.android.tools.r8.utils.SegmentTree; | 
|  | import com.android.tools.r8.utils.ThrowingConsumer; | 
|  | import java.io.BufferedReader; | 
|  | import java.io.Closeable; | 
|  | import java.io.IOException; | 
|  | import java.io.StringReader; | 
|  | import java.util.ArrayList; | 
|  | import java.util.HashMap; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  |  | 
|  | // This is a parser for the Kotlin-generated source debug extensions, which is a stratified map. | 
|  | // The kotlin-parser for this data is can be found here in the kotlin compiler: | 
|  | // compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SMAPParser.kt | 
|  | public class KotlinSourceDebugExtensionParser { | 
|  |  | 
|  | private static final String SMAP_IDENTIFIER = "SMAP"; | 
|  | private static final String SMAP_SECTION_START = "*S"; | 
|  | private static final String SMAP_SECTION_KOTLIN_START = SMAP_SECTION_START + " Kotlin"; | 
|  | private static final String SMAP_FILES_IDENTIFIER = "*F"; | 
|  | private static final String SMAP_LINES_IDENTIFIER = "*L"; | 
|  | private static final String SMAP_END_IDENTIFIER = "*E"; | 
|  |  | 
|  | public static class KotlinSourceDebugExtensionParserException extends Exception { | 
|  |  | 
|  | KotlinSourceDebugExtensionParserException(String message) { | 
|  | super(message); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class BufferedStringReader implements Closeable { | 
|  |  | 
|  | private final BufferedReader reader; | 
|  |  | 
|  | private String readLine; | 
|  |  | 
|  | BufferedStringReader(String data) { | 
|  | reader = new BufferedReader(new StringReader(data)); | 
|  | } | 
|  |  | 
|  | String readNextLine() throws IOException { | 
|  | return readLine = reader.readLine(); | 
|  | } | 
|  |  | 
|  | boolean readExpectedLine(String expected) throws IOException { | 
|  | return readNextLine().equals(expected); | 
|  | } | 
|  |  | 
|  | void readExpectedLineOrThrow(String expected) | 
|  | throws KotlinSourceDebugExtensionParserException, IOException { | 
|  | if (!readExpectedLine(expected)) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "The string " + readLine + " does not match the expected string " + expected); | 
|  | } | 
|  | } | 
|  |  | 
|  | boolean isEOF() { | 
|  | return readLine == null; | 
|  | } | 
|  |  | 
|  | BufferedStringReader readUntil(String terminator) throws IOException { | 
|  | while (!terminator.equals(readLine) && !isEOF()) { | 
|  | readNextLine(); | 
|  | } | 
|  | if (isEOF()) { | 
|  | return this; | 
|  | } | 
|  | return this; | 
|  | } | 
|  |  | 
|  | void readUntil( | 
|  | String terminator, | 
|  | int linesInBlock, | 
|  | ThrowingConsumer<List<String>, KotlinSourceDebugExtensionParserException> callback) | 
|  | throws IOException, KotlinSourceDebugExtensionParserException { | 
|  | if (terminator.equals(readLine)) { | 
|  | return; | 
|  | } | 
|  | List<String> readStrings = new ArrayList<>(); | 
|  | readStrings.add(readNextLine()); | 
|  | int linesLeft = linesInBlock; | 
|  | while (!terminator.equals(readLine) && !isEOF()) { | 
|  | if (linesLeft == 1) { | 
|  | assert readStrings.size() == linesInBlock; | 
|  | callback.accept(readStrings); | 
|  | linesLeft = linesInBlock; | 
|  | readStrings = new ArrayList<>(); | 
|  | } else { | 
|  | linesLeft -= 1; | 
|  | } | 
|  | readStrings.add(readNextLine()); | 
|  | } | 
|  | if (readStrings.size() > 0 && !readStrings.get(0).equals(terminator)) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Block size does not match linesInBlock = " + linesInBlock); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void close() throws IOException { | 
|  | reader.close(); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static Result parse(String annotationData) { | 
|  | if (annotationData == null || annotationData.isEmpty()) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // The source debug smap data is on the following form: | 
|  | // SMAP | 
|  | // <filename> | 
|  | // Kotlin <-- why is this even here? | 
|  | // *S <section> | 
|  | // *F | 
|  | // + <file_id_i> <file_name_i> | 
|  | // <path_i> | 
|  | // *L | 
|  | // <from>#<file>,<to>:<debug-line-position> | 
|  | // <from>#<file>:<debug-line-position> | 
|  | // *E | 
|  | // *S KotlinDebug | 
|  | // *** | 
|  | // *E | 
|  | try (BufferedStringReader reader = new BufferedStringReader(annotationData)) { | 
|  | ResultBuilder builder = new ResultBuilder(); | 
|  | // Check for SMAP | 
|  | if (!reader.readExpectedLine(SMAP_IDENTIFIER)) { | 
|  | return null; | 
|  | } | 
|  | if (reader.readUntil(SMAP_SECTION_KOTLIN_START).isEOF()) { | 
|  | return null; | 
|  | } | 
|  | // At this point we should be parsing a kotlin source debug extension, so we will throw if we | 
|  | // read an unexpected line. | 
|  | reader.readExpectedLineOrThrow(SMAP_FILES_IDENTIFIER); | 
|  | // Iterate over the files section with the format: | 
|  | // + <file_number_i> <file_name_i> | 
|  | // <file_path_i> | 
|  | reader.readUntil( | 
|  | SMAP_LINES_IDENTIFIER, 2, block -> addFileToBuilder(block.get(0), block.get(1), builder)); | 
|  |  | 
|  | // Ensure that we read *L. | 
|  | if (reader.isEOF()) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Unexpected EOF - no debug line positions"); | 
|  | } | 
|  | // Iterate over the debug line number positions: | 
|  | // <from>#<file>,<range>:<debug-line-position> | 
|  | // or | 
|  | // <from>#<file>:<debug-line-position> | 
|  | reader.readUntil( | 
|  | SMAP_END_IDENTIFIER, 1, block -> addDebugEntryToBuilder(block.get(0), builder)); | 
|  |  | 
|  | // Ensure that we read the end section *E. | 
|  | if (reader.isEOF()) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Unexpected EOF when parsing SMAP debug entries"); | 
|  | } | 
|  |  | 
|  | return builder.build(); | 
|  | } catch (IOException | KotlinSourceDebugExtensionParserException e) { | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | private static void addFileToBuilder(String entryLine, String filePath, ResultBuilder builder) | 
|  | throws KotlinSourceDebugExtensionParserException { | 
|  | // + <file_number_i> <file_name_i> | 
|  | // <file_path_i> | 
|  | String[] entries = entryLine.trim().split(" "); | 
|  | if (entries.length != 3 || !entries[0].equals("+")) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Wrong number of entries on line " + entryLine); | 
|  | } | 
|  | String fileName = entries[2]; | 
|  | if (fileName.isEmpty()) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Did not expect file name to be empty for line " + entryLine); | 
|  | } | 
|  | if (filePath == null || filePath.isEmpty()) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Did not expect file path to be null or empty for " + entryLine); | 
|  | } | 
|  | int index = asInteger(entries[1]); | 
|  | Source source = new Source(fileName, filePath); | 
|  | Source existingSource = builder.files.put(index, source); | 
|  | if (existingSource != null) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "File index " + index + " was already mapped to an existing source: " + source); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static int asInteger(String numberAsString) | 
|  | throws KotlinSourceDebugExtensionParserException { | 
|  | int number = -1; | 
|  | try { | 
|  | number = Integer.parseInt(numberAsString); | 
|  | } catch (NumberFormatException e) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Could not parse number " + numberAsString); | 
|  | } | 
|  | return number; | 
|  | } | 
|  |  | 
|  | private static void addDebugEntryToBuilder(String debugEntry, ResultBuilder builder) | 
|  | throws KotlinSourceDebugExtensionParserException { | 
|  | // <from>#<file>,<size>:<debug-line-position> | 
|  | // or | 
|  | // <from>#<file>:<debug-line-position> | 
|  | // All positions should define intervals for mappings. | 
|  | try { | 
|  | int targetSplit = debugEntry.indexOf(':'); | 
|  | int target = Integer.parseInt(debugEntry.substring(targetSplit + 1)); | 
|  | String original = debugEntry.substring(0, targetSplit); | 
|  | int fileIndexSplit = original.indexOf('#'); | 
|  | int originalStart = Integer.parseInt(original.substring(0, fileIndexSplit)); | 
|  | // The range may have a different end than start. | 
|  | String fileAndEndRange = original.substring(fileIndexSplit + 1); | 
|  | int endRangeCharPosition = fileAndEndRange.indexOf(','); | 
|  | int size = 1; | 
|  | if (endRangeCharPosition > -1) { | 
|  | // The file should be at least one number wide. | 
|  | assert endRangeCharPosition > 0; | 
|  | size = Integer.parseInt(fileAndEndRange.substring(endRangeCharPosition + 1)); | 
|  | } else { | 
|  | endRangeCharPosition = fileAndEndRange.length(); | 
|  | } | 
|  | int fileIndex = Integer.parseInt(fileAndEndRange.substring(0, endRangeCharPosition)); | 
|  | Source thisFileSource = builder.files.get(fileIndex); | 
|  | if (thisFileSource == null) { | 
|  | throw new KotlinSourceDebugExtensionParserException( | 
|  | "Could not find file with index " + fileIndex); | 
|  | } | 
|  | Range range = new Range(originalStart, originalStart + (size - 1)); | 
|  | Position position = new Position(thisFileSource, range); | 
|  | builder.segmentTree.add(target, target + (size - 1), position); | 
|  | } catch (NumberFormatException e) { | 
|  | throw new KotlinSourceDebugExtensionParserException("Could not convert position to number"); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class Result { | 
|  |  | 
|  | private final SegmentTree<Position> segmentTree; | 
|  |  | 
|  | public Result(SegmentTree<Position> segmentTree) { | 
|  | this.segmentTree = segmentTree; | 
|  | } | 
|  |  | 
|  | public Map.Entry<Integer, Position> lookup(int point) { | 
|  | return segmentTree.findEntry(point); | 
|  | } | 
|  |  | 
|  | public int size() { | 
|  | return segmentTree.size(); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class ResultBuilder { | 
|  |  | 
|  | SegmentTree<Position> segmentTree = new SegmentTree<>(false); | 
|  | Map<Integer, Source> files = new HashMap<>(); | 
|  |  | 
|  | public Result build() { | 
|  | return new Result(segmentTree); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class Source { | 
|  | private final String fileName; | 
|  | private final String path; | 
|  |  | 
|  | private Source(String fileName, String path) { | 
|  | this.fileName = fileName; | 
|  | this.path = path; | 
|  | } | 
|  |  | 
|  | public String getFileName() { | 
|  | return fileName; | 
|  | } | 
|  |  | 
|  | public String getPath() { | 
|  | return path; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return path + "(" + fileName + ")"; | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class Position { | 
|  | private final Source source; | 
|  | private final Range range; | 
|  |  | 
|  | public Position(Source source, Range range) { | 
|  | this.source = source; | 
|  | this.range = range; | 
|  | } | 
|  |  | 
|  | public Source getSource() { | 
|  | return source; | 
|  | } | 
|  |  | 
|  | public Range getRange() { | 
|  | return range; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | StringBuilder sb = new StringBuilder(); | 
|  | sb.append(range.from); | 
|  | sb.append("#"); | 
|  | sb.append(source); | 
|  | if (range.to != range.from) { | 
|  | sb.append(","); | 
|  | sb.append(range.to); | 
|  | } | 
|  | sb.append(":"); | 
|  | return sb.toString(); | 
|  | } | 
|  | } | 
|  | } |