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

Commit 57d9799d authored by John Wu's avatar John Wu
Browse files

[HostStubGen] Make HSG more modularize

In preparation for adding class processing to the ravenizer tool to
apply several bytecode transformations available in the main HSG tool,
update the code base to be more modular.

- Make hoststubgen-lib self contained.
- Extract the bytecode transformation logic into its own class
  HostStubGenClassProcessor.
- Create BaseOptions to standardize commandline parsing and allow option
  inheritance to process options that can be shared across tools.
- Make hoststubgen fully a client of hoststubgen-lib to enforce
  encapsulation within the codebase.

With these changes in place, it would be easier to update ravenizer to
directly utilize HostStubGenClassProcessor in a follow-up CL.

Bug: 397498134
Flag: EXEMPT host side change only
Test: f/b/r/scripts/run-ravenwood-tests.sh
Change-Id: Ib46e322a9ddd22375b598339d36b9ed71325dc16
parent 43f10ef5
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