mikepenz/AboutLibraries

Don't crash on missing data - Feature request

TSurkis opened this issue ยท 10 comments

About this issue

I've used this library extensively and on rare occasions the value of name for a library won't exist. It is weird then, that the entire parsing mechanism crashes on a value that doesn't seem that important.

We can provide a new flag that would represent a recoverability option for those who do not wish for the entire parsing method to crash. This flag will default values for non null fields that ended up being null:

  • name - can be replaced by "uniqueId" which represents the package name.
  • developers - can be replaced by an emptyList() if none exist.

Checklist

Thank you very much for the report.

I agree, there should not be a crash in those cases, and that it's a good idea to make it more secure.

Do you have an example for a dependency which misses the title, I'd like to check the plugin itself, given the data generation should already handle some parts of this.

@mikepenz No example as of now sadly. I am opening a PR shortly with an offered solution.

@mikepenz I have no permissions, even pushing to a side branch ๐Ÿ™ˆ

Here's my suggested solution:

Libs.kt

data class Libs constructor(
    val libraries: List<Library>,
    val licenses: Set<License>,
) {
    /**
     * Builder used to automatically parse and interpret the generated library data from the plugin.
     */
    class Builder {
        private var _stringData: String? = null
        private var recoverable: Boolean = false

        /**
         * Provide the generated library data as [String]
         */
        fun withJson(stringData: String): Builder {
            _stringData = stringData
            return this
        }

        /**
         * Don't crash on missing library data. Instead fill in:
         * [Library.name] = use [Library.uniqueId]
         * [Library.developers] = use [emptyList]
         */
        fun recoverableMissingData(recoverable: Boolean): Builder {
            this.recoverable = recoverable
            return this
        }

        /**
         * Build the [Libs] instance with the applied configuration.
         */
        fun build(): Libs {
            val data = _stringData
            val (libraries, licenses) = if (data != null) {
                parseData(data, recoverable)
            } else {
                throw IllegalStateException(
                    """
                    Please provide the required library data via the available APIs.
                    Depending on the platform this can be done for example via `LibsBuilder().withJson()`.
                    For Android there exists an `LibsBuilder.withContext()`, automatically loading the `aboutlibraries.json` file from the `raw` resources folder.
                    When using compose or other parent modules, please check their corresponding APIs.
                """.trimIndent()
                )
            }
            return Libs(libraries.sortedBy { it.name.lowercase() }, licenses.toMutableSet())
        }
    }
}

AndroidParser.kt

actual fun parseData(json: String, recoverable: Boolean): Result {
    try {
        val metaData = JSONObject(json)

        val licenses = metaData.getJSONObject("licenses").forEachObject { key ->
            License(
                getString("name"),
                optString("url"),
                optString("year"),
                optString("spdxId"),
                optString("content"),
                key
            )
        }
        val mappedLicenses = licenses.associateBy { it.hash }
        val libraries = metaData.getJSONArray("libraries").forEachObject {
            val libLicenses = optJSONArray("licenses").forEachString { mappedLicenses[this] }.mapNotNull { it }.toHashSet()
            val developers = optJSONArray("developers")?.forEachObject {
                Developer(optString("name"), optString("organisationUrl"))
            } ?: emptyList()
            val organization = optJSONObject("organization")?.let {
                Organization(it.getString("name"), it.optString("url"))
            }
            val scm = optJSONObject("scm")?.let {
                Scm(it.optString("connection"), it.optString("developerConnection"), it.optString("url"))
            }
            val funding = optJSONArray("funding").forEachObject {
                Funding(getString("platform"), getString("url"))
            }.toSet()
            val id = getString("uniqueId")
            Library(
                id,
                optString("artifactVersion"),
                if (recoverable) optString("name", id) else getString("name"),
                optString("description"),
                optString("website"),
                developers,
                organization,
                scm,
                libLicenses,
                funding,
                optString("tag")
            )
        }
        return Result(libraries, licenses)
    } catch (t: Throwable) {
        Log.e("AboutLibraries", "Failed to parse the meta data *.json file: $t")
    }
    return Result(emptyList(), emptyList())
}

MultiplatformParser.kt

actual fun parseData(json: String, recoverable: Boolean): Result {
    try {
        val metaData = Json.parseToJsonElement(json).jsonObject

        val licenses = metaData.getJSONObject("licenses").forEachObject { key ->
            License(
                getString("name"),
                optString("url"),
                optString("year"),
                optString("spdxId"),
                optString("content"),
                key
            )
        }
        val mappedLicenses = licenses.associateBy { it.hash }
        val libraries = metaData.getJSONArray("libraries").forEachObject {
            val libLicenses = optJSONArray("licenses").forEachString { mappedLicenses[this] }.mapNotNull { it }.toHashSet()
            val developers = optJSONArray("developers")?.forEachObject {
                Developer(optString("name"), optString("organisationUrl"))
            } ?: emptyList()
            val organization = optJSONObject("organization")?.let {
                Organization(it.getString("name"), it.optString("url"))
            }
            val scm = optJSONObject("scm")?.let {
                Scm(it.optString("connection"), it.optString("developerConnection"), it.optString("url"))
            }
            val funding = optJSONArray("funding").forEachObject {
                Funding(getString("platform"), getString("url"))
            }.toSet()

            val id = getString("uniqueId")
            Library(
                id,
                optString("artifactVersion"),
                if (recoverable) optString("name") ?: id else getString("name"),
                optString("description"),
                optString("website"),
                developers,
                organization,
                scm,
                libLicenses,
                funding,
                optString("tag")
            )
        }
        return Result(libraries, licenses)
    } catch (t: Throwable) {
        println("Failed to parse the meta data *.json file: $t")
    }
    return Result(emptyList(), emptyList())
}

The easiest way to reproduce is to put these lines after the first line in the parseData method

        val firstItem: JSONObject = metaData.getJSONArray("libraries")[0] as JSONObject
        firstItem.remove("name")
        firstItem.remove("developers")

@TSurkis you'll have to fork the project, create a branch on your fork make the modifications. And then you can open a PR with it :)

https://docs.github.com/en/get-started/quickstart/contributing-to-projects

@mikepenz Oh yes right. Never done one before ๐Ÿ˜Ž
#880

@mikepenz I've added the required changes: #880

Thank you very much

@mikepenz Thanks! Is there a timeline or an estimate to when this will be released? ๐Ÿ˜Ž

Available as part of 10.7.0 (currently being synced to maven central)