Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3972270a authored by John Wu's avatar John Wu Committed by Android (Google) Code Review
Browse files

Merge "[HostStubGen] Make HSG more modularize" into main

parents de267273 57d9799d
Loading
Loading
Loading
Loading
+3 −12
Original line number Diff line number Diff line
@@ -90,11 +90,9 @@ java_library {
java_library_host {
    name: "hoststubgen-lib",
    defaults: ["ravenwood-internal-only-visibility-java"],
    srcs: ["src/**/*.kt"],
    srcs: ["lib/**/*.kt"],
    static_libs: [
        "hoststubgen-helper-runtime",
    ],
    libs: [
        "junit",
        "ow2-asm",
        "ow2-asm-analysis",
@@ -108,15 +106,8 @@ java_library_host {
java_binary_host {
    name: "hoststubgen",
    main_class: "com.android.hoststubgen.HostStubGenMain",
    static_libs: [
        "hoststubgen-lib",
        "junit",
        "ow2-asm",
        "ow2-asm-analysis",
        "ow2-asm-commons",
        "ow2-asm-tree",
        "ow2-asm-util",
    ],
    srcs: ["src/**/*.kt"],
    static_libs: ["hoststubgen-lib"],
    visibility: ["//visibility:public"],
}

+165 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.hoststubgen

import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.filters.AnnotationBasedFilter
import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter
import com.android.hoststubgen.filters.ConstantFilter
import com.android.hoststubgen.filters.DefaultHookInjectingFilter
import com.android.hoststubgen.filters.FilterRemapper
import com.android.hoststubgen.filters.ImplicitOutputFilter
import com.android.hoststubgen.filters.KeepNativeFilter
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.SanitizationFilter
import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
import com.android.hoststubgen.utils.ClassPredicate
import com.android.hoststubgen.visitors.BaseAdapter
import com.android.hoststubgen.visitors.PackageRedirectRemapper
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.util.CheckClassAdapter

/**
 * This class implements bytecode transformation of HostStubGen.
 */
class HostStubGenClassProcessor(
    private val options: HostStubGenClassProcessorOptions,
    private val allClasses: ClassNodes,
    private val errors: HostStubGenErrors = HostStubGenErrors(),
    private val stats: HostStubGenStats? = null,
) {
    val filter = buildFilter()
    val remapper = FilterRemapper(filter)

    private val packageRedirector = PackageRedirectRemapper(options.packageRedirects)

    /**
     * Build the filter, which decides what classes/methods/fields should be put in stub or impl
     * jars, and "how". (e.g. with substitution?)
     */
    private fun buildFilter(): OutputFilter {
        // We build a "chain" of multiple filters here.
        //
        // The filters are build in from "inside", meaning the first filter created here is
        // the last filter used, so it has the least precedence.
        //
        // So, for example, the "remove" annotation, which is handled by AnnotationBasedFilter,
        // can override a class-wide annotation, which is handled by
        // ClassWidePolicyPropagatingFilter, and any annotations can be overridden by the
        // text-file based filter, which is handled by parseTextFilterPolicyFile.

        // The first filter is for the default policy from the command line options.
        var filter: OutputFilter = ConstantFilter(options.defaultPolicy.get, "default-by-options")

        // Next, we build a filter that preserves all native methods by default
        filter = KeepNativeFilter(allClasses, filter)

        // Next, we need a filter that resolves "class-wide" policies.
        // This is used when a member (methods, fields, nested classes) don't get any policies
        // from upper filters. e.g. when a method has no annotations, then this filter will apply
        // the class-wide policy, if any. (if not, we'll fall back to the above filter.)
        filter = ClassWidePolicyPropagatingFilter(allClasses, filter)

        // Inject default hooks from options.
        filter = DefaultHookInjectingFilter(
            allClasses,
            options.defaultClassLoadHook.get,
            options.defaultMethodCallHook.get,
            filter
        )

        val annotationAllowedPredicate = options.annotationAllowedClassesFile.get.let { file ->
            if (file == null) {
                ClassPredicate.newConstantPredicate(true) // Allow all classes
            } else {
                ClassPredicate.loadFromFile(file, false)
            }
        }

        // Next, Java annotation based filter.
        val annotFilter = AnnotationBasedFilter(
            errors,
            allClasses,
            options.keepAnnotations,
            options.keepClassAnnotations,
            options.throwAnnotations,
            options.removeAnnotations,
            options.ignoreAnnotations,
            options.substituteAnnotations,
            options.redirectAnnotations,
            options.redirectionClassAnnotations,
            options.classLoadHookAnnotations,
            options.partiallyAllowedAnnotations,
            options.keepStaticInitializerAnnotations,
            annotationAllowedPredicate,
            filter
        )
        filter = annotFilter

        // Next, "text based" filter, which allows to override policies without touching
        // the target code.
        if (options.policyOverrideFiles.isNotEmpty()) {
            val builder = TextFileFilterPolicyBuilder(allClasses, filter)
            options.policyOverrideFiles.forEach(builder::parse)
            filter = builder.createOutputFilter()
            annotFilter.annotationAllowedMembers = builder.annotationAllowedMembersFilter
        }

        // Apply the implicit filter.
        filter = ImplicitOutputFilter(errors, allClasses, filter)

        // Add a final sanitization step.
        filter = SanitizationFilter(errors, allClasses, filter)

        return filter
    }

    fun processClassBytecode(bytecode: ByteArray): ByteArray {
        val cr = ClassReader(bytecode)

        // COMPUTE_FRAMES wouldn't be happy if code uses
        val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
        val cw = ClassWriter(flags)

        // Connect to the class writer
        var outVisitor: ClassVisitor = cw
        if (options.enableClassChecker.get) {
            outVisitor = CheckClassAdapter(outVisitor)
        }

        // Remapping should happen at the end.
        outVisitor = ClassRemapper(outVisitor, remapper)

        val visitorOptions = BaseAdapter.Options(
            errors = errors,
            stats = stats,
            enablePreTrace = options.enablePreTrace.get,
            enablePostTrace = options.enablePostTrace.get,
            deleteClassFinals = options.deleteFinals.get,
            deleteMethodFinals = options.deleteFinals.get,
        )
        outVisitor = BaseAdapter.getVisitor(
            cr.className, allClasses, outVisitor, filter,
            packageRedirector, visitorOptions
        )

        cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }
}
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.hoststubgen

import com.android.hoststubgen.filters.FilterPolicy
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce

private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
    val colon = fromColonTo.indexOf(':')
    if ((colon < 1) || (colon + 1 >= fromColonTo.length)) {
        throw ArgumentsException("--package-redirect must be a colon-separated string")
    }
    // TODO check for duplicates
    return Pair(fromColonTo.substring(0, colon), fromColonTo.substring(colon + 1))
}

/**
 * Options to configure [HostStubGenClassProcessor].
 */
open class HostStubGenClassProcessorOptions(
    var keepAnnotations: MutableSet<String> = mutableSetOf(),
    var throwAnnotations: MutableSet<String> = mutableSetOf(),
    var removeAnnotations: MutableSet<String> = mutableSetOf(),
    var ignoreAnnotations: MutableSet<String> = mutableSetOf(),
    var keepClassAnnotations: MutableSet<String> = mutableSetOf(),
    var partiallyAllowedAnnotations: MutableSet<String> = mutableSetOf(),
    var redirectAnnotations: MutableSet<String> = mutableSetOf(),

    var substituteAnnotations: MutableSet<String> = mutableSetOf(),
    var redirectionClassAnnotations: MutableSet<String> = mutableSetOf(),
    var classLoadHookAnnotations: MutableSet<String> = mutableSetOf(),
    var keepStaticInitializerAnnotations: MutableSet<String> = mutableSetOf(),

    var packageRedirects: MutableList<Pair<String, String>> = mutableListOf(),

    var annotationAllowedClassesFile: SetOnce<String?> = SetOnce(null),

    var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
    var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),

    var policyOverrideFiles: MutableList<String> = mutableListOf(),

    var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),

    var deleteFinals: SetOnce<Boolean> = SetOnce(false),

    var enableClassChecker: SetOnce<Boolean> = SetOnce(false),
    var enablePreTrace: SetOnce<Boolean> = SetOnce(false),
    var enablePostTrace: SetOnce<Boolean> = SetOnce(false),
) : BaseOptions() {

    private val allAnnotations = mutableSetOf<String>()

    private fun ensureUniqueAnnotation(name: String): String {
        if (!allAnnotations.add(name)) {
            throw DuplicateAnnotationException(name)
        }
        return name
    }

    override fun parseOption(option: String, ai: ArgIterator): Boolean {
        // Define some shorthands...
        fun nextArg(): String = ai.nextArgRequired(option)
        fun MutableSet<String>.addUniqueAnnotationArg(): String =
            nextArg().also { this += ensureUniqueAnnotation(it) }

        when (option) {
            "--policy-override-file" ->
                policyOverrideFiles.add(nextArg().ensureFileExists())

            "--default-remove" -> defaultPolicy.set(FilterPolicy.Remove)
            "--default-throw" -> defaultPolicy.set(FilterPolicy.Throw)
            "--default-keep" -> defaultPolicy.set(FilterPolicy.Keep)

            "--keep-annotation" ->
                keepAnnotations.addUniqueAnnotationArg()

            "--keep-class-annotation" ->
                keepClassAnnotations.addUniqueAnnotationArg()

            "--partially-allowed-annotation" ->
                partiallyAllowedAnnotations.addUniqueAnnotationArg()

            "--throw-annotation" ->
                throwAnnotations.addUniqueAnnotationArg()

            "--remove-annotation" ->
                removeAnnotations.addUniqueAnnotationArg()

            "--ignore-annotation" ->
                ignoreAnnotations.addUniqueAnnotationArg()

            "--substitute-annotation" ->
                substituteAnnotations.addUniqueAnnotationArg()

            "--redirect-annotation" ->
                redirectAnnotations.addUniqueAnnotationArg()

            "--redirection-class-annotation" ->
                redirectionClassAnnotations.addUniqueAnnotationArg()

            "--class-load-hook-annotation" ->
                classLoadHookAnnotations.addUniqueAnnotationArg()

            "--keep-static-initializer-annotation" ->
                keepStaticInitializerAnnotations.addUniqueAnnotationArg()

            "--package-redirect" ->
                packageRedirects += parsePackageRedirect(nextArg())

            "--annotation-allowed-classes-file" ->
                annotationAllowedClassesFile.set(nextArg())

            "--default-class-load-hook" ->
                defaultClassLoadHook.set(nextArg())

            "--default-method-call-hook" ->
                defaultMethodCallHook.set(nextArg())

            "--delete-finals" -> deleteFinals.set(true)

            // Following options are for debugging.
            "--enable-class-checker" -> enableClassChecker.set(true)
            "--no-class-checker" -> enableClassChecker.set(false)

            "--enable-pre-trace" -> enablePreTrace.set(true)
            "--no-pre-trace" -> enablePreTrace.set(false)

            "--enable-post-trace" -> enablePostTrace.set(true)
            "--no-post-trace" -> enablePostTrace.set(false)

            else -> return false
        }

        return true
    }

    override fun dumpFields(): String {
        return """
            keepAnnotations=$keepAnnotations,
            throwAnnotations=$throwAnnotations,
            removeAnnotations=$removeAnnotations,
            ignoreAnnotations=$ignoreAnnotations,
            keepClassAnnotations=$keepClassAnnotations,
            partiallyAllowedAnnotations=$partiallyAllowedAnnotations,
            substituteAnnotations=$substituteAnnotations,
            nativeSubstituteAnnotations=$redirectionClassAnnotations,
            classLoadHookAnnotations=$classLoadHookAnnotations,
            keepStaticInitializerAnnotations=$keepStaticInitializerAnnotations,
            packageRedirects=$packageRedirects,
            annotationAllowedClassesFile=$annotationAllowedClassesFile,
            defaultClassLoadHook=$defaultClassLoadHook,
            defaultMethodCallHook=$defaultMethodCallHook,
            policyOverrideFiles=${policyOverrideFiles.toTypedArray().contentToString()},
            defaultPolicy=$defaultPolicy,
            deleteFinals=$deleteFinals,
            enableClassChecker=$enableClassChecker,
            enablePreTrace=$enablePreTrace,
            enablePostTrace=$enablePostTrace,
        """.trimIndent()
    }
}
Loading