blob: 89550eaf3fc61ce1bf74f318d36296dcb4a3b371 [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* 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.
*/
package androidx.build.libabigail.symbolfiles
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
class ParseError(message: String) : RuntimeException(message)
typealias Tags = List<String>
data class Version(
val name: String,
val base: String? = null,
val symbols: List<Symbol> = emptyList()
)
data class Symbol(val name: String)
/**
* Class to parse version scripts based on the original python implementation but simplified to
* ignore tags and some other unused parameters.
*
* https://android.googlesource.com/platform/build/soong/+/master/cc/symbolfile/__init__.py
*/
class SymbolFileParser(
val input: InputStream
) {
val reader = BufferedReader(InputStreamReader(input))
lateinit var currentLine: String
init {
nextLine()
}
fun parse(): List<Version> {
val versions = mutableListOf<Version>()
do {
if (currentLine.contains("{")) {
versions.add(parseNextVersion())
} else {
throw ParseError("Unexpected contents at top level: $currentLine")
}
} while (nextLine().isNotBlank())
return versions
}
/**
* Parses a single version section and returns a Version object.
*/
fun parseNextVersion(): Version {
val name = currentLine.split("{").first().trim()
val symbols = mutableListOf<Symbol>()
var globalScope = true
var cppSymbols = false
while (nextLine().isNotBlank()) {
if (currentLine.contains("}")) {
// Line is something like '} BASE; # tags'. Both base and tags are optional here.
val base = currentLine.split("}").last().split("#").first().trim()
if (!base.endsWith(";")) {
throw ParseError("Unterminated version/export \"C++\" block (expected ;).")
}
if (cppSymbols) {
cppSymbols = false
} else {
return Version(
name,
base.removeSuffix(";").trim().ifBlank { null },
symbols
)
}
} else if (currentLine.contains("extern \"C++\" {")) {
cppSymbols = true
} else if (!cppSymbols && currentLine.contains(":")) {
val visibility = currentLine.split(':').first().trim()
if (visibility == "local") {
globalScope = false
} else if (visibility == "global") {
globalScope = true
} else {
throw ParseError("Unknown visibility label: $visibility")
}
} else if (globalScope) {
symbols.add(parseNextSymbol(cppSymbols))
} else {
// We're in a hidden scope. Ignore everything.
}
}
throw ParseError("Unexpected EOF in version block.")
}
/**
* Parses a single symbol line and returns a Symbol object.
*/
fun parseNextSymbol(cppSymbol: Boolean): Symbol {
val line = currentLine
if (!line.contains(";")) {
throw ParseError("Expected ; to terminate symbol: ${line.trim()}")
}
if (line.contains("*")) {
throw ParseError("Wildcard global symbols are not permitted.")
}
// Line is now in the format "<symbol-name>; # tags"
val name = line.trim().split(";").first().let {
if (cppSymbol) {
it.removeSurrounding("\"")
} else {
it
}
}
return Symbol(name)
}
/**
* Returns the next non-empty non-comment line.
* A return value of '' indicates EOF.
*/
fun nextLine(): String {
var line: String? = reader.readLine()
while (line != null && (line.isBlank() || line.trim().startsWith("#"))) {
line = reader.readLine() ?: break
}
currentLine = line ?: ""
return currentLine
}
}