blob: 45b736c248ca52dacbe8db920937b094ffbe7bcc [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.android.tools.lint.checks
import com.android.ide.common.repository.GradleCoordinate
import com.android.ide.common.repository.GradleVersion
import com.android.ide.common.repository.NetworkCache
import com.android.tools.lint.detector.api.Severity
import com.android.utils.XmlUtils.parseDocument
import com.android.utils.iterator
import org.w3c.dom.Element
import org.xml.sax.SAXException
import java.io.BufferedReader
import java.io.File
import java.io.InputStream
import java.io.InputStreamReader
import kotlin.text.Charsets.UTF_8
/** Remote URL for current SDK metadata */
const val SDK_REGISTRY_URL =
"https://dl.google.com/dl/android/sdk/metadata/sdk_registry.xml"
/** Key used in cache directories to locate the deprecated SDK network cache */
const val DEPRECATED_SDK_CACHE_DIR_KEY = "sdk-registry.xml"
private const val TAG_ROOT = "sdk_metadata"
private const val TAG_LIBRARY = "library"
private const val TAG_VERSIONS = "versions"
private const val ATTR_GROUP_ID = "groupId"
private const val ATTR_ARTIFACT_ID = "artifactId"
private const val ATTR_FROM = "from"
private const val ATTR_TO = "to"
private const val ATTR_RECOMMENDED_VERSION = "recommended-version"
private const val ATTR_STATUS = "status"
private const val ATTR_DESCRIPTION = "description"
/**
* Information about deprecated and vulnerable libraries, based on
* metadata published on dl.google.com:
* https://dl.google.com/dl/android/sdk/metadata/sdk_registry.xml
*/
abstract class DeprecatedSdkRegistry(
/** Location to search for cached repository content files */
cacheDir: File? = null
) : NetworkCache(
SDK_REGISTRY_URL,
DEPRECATED_SDK_CACHE_DIR_KEY,
cacheDir
) {
private val groupToArtifactToElement: MutableMap<String, MutableList<Pair<String, Element>>> =
HashMap(100)
fun initialize(stream: InputStream) {
val document = try {
parseDocument(BufferedReader(InputStreamReader(stream, UTF_8)), false)
} catch (e: SAXException) {
// Malformed XML. Most likely the file we received was not the XML file
// but some sort of network portal redirect HTML page. Gracefully degrade.
return
}
val root = document.documentElement
assert(root.tagName == TAG_ROOT, { root.tagName })
for (library in root) {
if (library.tagName != TAG_LIBRARY) { // Tolerate future extra tags
continue
}
val groupId = library.getAttribute(ATTR_GROUP_ID)
val artifactId = library.getAttribute(ATTR_ARTIFACT_ID)
val artifactList: MutableList<Pair<String, Element>> =
groupToArtifactToElement[groupId] ?: run {
val list: MutableList<Pair<String, Element>> = mutableListOf()
groupToArtifactToElement[groupId] = list
list
}
artifactList.add(Pair(artifactId, library))
}
}
/** Returns the latest recommended version */
fun getRecommendedVersion(dependency: GradleCoordinate): GradleVersion? {
val library = findDeclaration(dependency) ?: return null
val recommendedVersion = library.getAttribute(ATTR_RECOMMENDED_VERSION)
return if (recommendedVersion.isBlank())
null
else {
GradleVersion.tryParse(recommendedVersion.removeSuffix("+"))
}
}
/** Finds the metadata element for the given coordinate */
fun findDeclaration(dependency: GradleCoordinate): Element? {
val groupId = dependency.groupId
val artifactId = dependency.artifactId
if (groupToArtifactToElement.isEmpty()) {
val stream = findData("") ?: return null
initialize(stream)
}
val artifactsInGroup = groupToArtifactToElement[groupId] ?: return null
for (artifact in artifactsInGroup) {
if (artifact.first == artifactId) {
return artifact.second
}
}
return null
}
/** Returns metadata about a given library if it is a known deprecated library */
fun getVersionInfo(dependency: GradleCoordinate): DeprecatedLibrary? {
val library = findDeclaration(dependency) ?: return null
val currentVersion = dependency.version ?: return null
for (versionElement in library) {
if (versionElement.tagName == TAG_VERSIONS) { // Tolerate future extra tags
val from = versionElement.getAttribute(ATTR_FROM)
val to = versionElement.getAttribute(ATTR_TO)
if (matches(currentVersion, from, to)) {
val recommendedVersion = library.getAttribute(ATTR_RECOMMENDED_VERSION)
val status = versionElement.getAttribute(ATTR_STATUS)
val description = versionElement.getAttribute(ATTR_DESCRIPTION)
val groupId = dependency.groupId
val artifactId = dependency.artifactId
if (recommendedVersion.isNotEmpty()) {
// In case the most recent (buggy) version is open ended, and there
// is a recommended version, we'll assume that the recommended version
// is fine.
val version = dependency.version
val recommended = GradleVersion.tryParse(recommendedVersion)
if (version != null && recommended != null && version >= recommended) {
return null
}
}
return DeprecatedLibrary(
groupId,
artifactId,
description,
status,
Severity.WARNING,
recommendedVersion
)
}
}
}
return null
}
/** Returns true if the given version is at least [fromString] and at most [toString] */
private fun matches(version: GradleVersion, fromString: String, toString: String): Boolean {
if (toString.isNotEmpty()) {
val to = GradleVersion.tryParse(toString)
if (to != null && version > to) {
return false
}
}
if (fromString.isNotEmpty()) {
val from = GradleVersion.tryParse(fromString)
if (from != null && version < from) {
return false
}
}
return true
}
override fun readDefaultData(relative: String): InputStream? {
assert(relative.isEmpty()) // only one file used in this cache
return DeprecatedSdkRegistry::class.java.getResourceAsStream("/sdks-offline.xml")
}
/** Metadata about a deprecated library */
data class DeprecatedLibrary(
val groupId: String,
val artifactId: String,
val message: String,
val status: String,
val severity: Severity,
val recommended: String?
)
}