PrivacySandboxApiPackager.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.tools.apipackager

import androidx.privacysandbox.tools.apipackager.AnnotationInspector.hasPrivacySandboxAnnotation
import java.nio.file.Path
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.notExists
import androidx.privacysandbox.tools.core.Metadata
import kotlin.io.path.extension
import kotlin.io.path.inputStream

class PrivacySandboxApiPackager {

    /**
     * Extracts API descriptors from SDKs annotated with Privacy Sandbox annotations.
     *
     * This function will output a file with SDK interface descriptors, which can later be used to
     * generate the client-side sources for communicating with this SDK over IPC through the
     * privacy sandbox.
     *
     * @param sdkClasspath path to the root directory that contains the SDK's compiled classes.
     *      Non-class files will be safely ignored.
     * @param sdkInterfaceDescriptorsOutput output path for SDK Interface descriptors file.
     */
    fun packageSdkDescriptors(
        sdkClasspath: Path,
        sdkInterfaceDescriptorsOutput: Path,
    ) {
        require(sdkClasspath.exists() && sdkClasspath.isDirectory()) {
            "$sdkClasspath is not a valid classpath."
        }
        require(sdkInterfaceDescriptorsOutput.notExists()) {
            "$sdkInterfaceDescriptorsOutput already exists."
        }

        val outputFile = sdkInterfaceDescriptorsOutput.toFile().also {
            it.parentFile.mkdirs()
            it.createNewFile()
        }

        ZipOutputStream(outputFile.outputStream()).use { zipOutputStream ->
            sdkClasspath.toFile().walk()
                .map { it.toPath() }
                .filter { shouldKeepFile(sdkClasspath, it) }
                .forEach { file ->
                    val zipEntry = ZipEntry(sdkClasspath.relativize(file).toString())
                    zipOutputStream.putNextEntry(zipEntry)
                    file.inputStream().copyTo(zipOutputStream)
                    zipOutputStream.closeEntry()
                }
        }
    }

    private fun shouldKeepFile(sdkClasspath: Path, filePath: Path): Boolean {
        if (sdkClasspath.relativize(filePath) == Metadata.filePath) {
            return true
        }
        if (filePath.extension == "class" && hasPrivacySandboxAnnotation(filePath)) {
            return true
        }
        return false
    }
}