SdkTableConfigParser.kt

/*
 * 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.privacysandbox.sdkruntime.client.config

import android.util.Xml
import androidx.annotation.RestrictTo
import java.io.InputStream
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParser.END_TAG
import org.xmlpull.v1.XmlPullParser.START_TAG
import org.xmlpull.v1.XmlPullParserException

/**
 * Parser for config with paths to compat SDK configs for each SDK that bundled with app.
 *
 * The expected XML structure is:
 * <runtime-enabled-sdk-table>
 *     <runtime-enabled-sdk>
 *         <package-name>com.sdk1</package-name>
 *         <version-major>1</version-major>
 *         <compat-config-path>assets/RuntimeEnabledSdk-com.sdk1/CompatSdkConfig.xml</compat-config-path>
 *     </runtime-enabled-sdk>
 *     <runtime-enabled-sdk>
 *         <package-name>com.sdk2</package-name>
 *         <version-major>42</version-major>
 *         <compat-config-path>assets/RuntimeEnabledSdk-com.sdk2/CompatSdkConfig.xml</compat-config-path>
 *     </runtime-enabled-sdk>
 * </runtime-enabled-sdk-table>
 *
 * @suppress
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class SdkTableConfigParser private constructor(
    private val xmlParser: XmlPullParser
) {

    private fun readSdkTable(): Set<SdkTableEntry> {
        xmlParser.require(XmlPullParser.START_DOCUMENT, NAMESPACE, null)
        xmlParser.nextTag()

        val packages = mutableSetOf<String>()

        return buildSet {
            xmlParser.require(START_TAG, NAMESPACE, SDK_TABLE_ELEMENT_NAME)
            while (xmlParser.next() != END_TAG) {
                if (xmlParser.eventType != START_TAG) {
                    continue
                }
                if (xmlParser.name == SDK_ENTRY_ELEMENT_NAME) {
                    val entry = readSdkEntry()
                    if (!packages.add(entry.packageName)) {
                        throw XmlPullParserException(
                            "Duplicate entry for ${entry.packageName} found"
                        )
                    }
                    add(entry)
                } else {
                    xmlParser.skipCurrentTag()
                }
            }
            xmlParser.require(END_TAG, NAMESPACE, SDK_TABLE_ELEMENT_NAME)
        }
    }

    private fun readSdkEntry(): SdkTableEntry {
        var packageName: String? = null
        var versionMajor: Int? = null
        var configPath: String? = null

        xmlParser.require(START_TAG, NAMESPACE, SDK_ENTRY_ELEMENT_NAME)
        while (xmlParser.next() != END_TAG) {
            if (xmlParser.eventType != START_TAG) {
                continue
            }
            when (xmlParser.name) {
                SDK_PACKAGE_NAME_ELEMENT_NAME -> {
                    if (packageName != null) {
                        throw XmlPullParserException(
                            "Duplicate $SDK_PACKAGE_NAME_ELEMENT_NAME tag found"
                        )
                    }
                    packageName = xmlParser.nextText()
                }

                VERSION_MAJOR_ELEMENT_NAME -> {
                    if (versionMajor != null) {
                        throw XmlPullParserException(
                            "Duplicate $VERSION_MAJOR_ELEMENT_NAME tag found"
                        )
                    }
                    versionMajor = xmlParser.nextText().toInt()
                }

                COMPAT_CONFIG_PATH_ELEMENT_NAME -> {
                    if (configPath != null) {
                        throw XmlPullParserException(
                            "Duplicate $COMPAT_CONFIG_PATH_ELEMENT_NAME tag found"
                        )
                    }
                    configPath = xmlParser.nextText()
                }

                else -> xmlParser.skipCurrentTag()
            }
        }
        xmlParser.require(END_TAG, NAMESPACE, SDK_ENTRY_ELEMENT_NAME)

        if (packageName == null) {
            throw XmlPullParserException(
                "No $SDK_PACKAGE_NAME_ELEMENT_NAME tag found"
            )
        }
        if (configPath == null) {
            throw XmlPullParserException(
                "No $COMPAT_CONFIG_PATH_ELEMENT_NAME tag found"
            )
        }

        return SdkTableEntry(packageName, versionMajor, configPath)
    }

    internal data class SdkTableEntry(
        val packageName: String,
        val versionMajor: Int?,
        val compatConfigPath: String,
    )

    companion object {
        private val NAMESPACE: String? = null // We don't use namespaces
        private const val SDK_TABLE_ELEMENT_NAME = "runtime-enabled-sdk-table"
        private const val SDK_ENTRY_ELEMENT_NAME = "runtime-enabled-sdk"
        private const val SDK_PACKAGE_NAME_ELEMENT_NAME = "package-name"
        private const val VERSION_MAJOR_ELEMENT_NAME = "version-major"
        private const val COMPAT_CONFIG_PATH_ELEMENT_NAME = "compat-config-path"

        fun parse(inputStream: InputStream): Set<SdkTableEntry> {
            val parser = Xml.newPullParser()
            try {
                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
                parser.setInput(inputStream, null)
                return SdkTableConfigParser(parser).readSdkTable()
            } finally {
                parser.setInput(null)
            }
        }
    }
}