SearchService

The search service allows you to search workspaces:

  • For strings, numbers, and references to classes and/or members
  • With the ability to cancel the search early
  • With the ability to control which classes and files are searched in
  • With the ability to control what results are included in the final output

Query model

All searches are built from Query instances. There are three types of queries:

  • AndroidClassQuery
  • JvmClassQuery
  • FileQuery

Each implementation creates a SearchVisitor that handles searching of individual items in the Workspace. Things like string, number, and reference searches all implement each of these query types that they are relevant for. For example the reference search only implements AndroidClassQuery and JvmClassQuery but string search implements all three since strings can appear in any of these places (classes and files).

Searching for common cases like strings, numbers, and references are already implemented as queries and take in predicates for matching content. The following examples assume the following services are injected:

@Inject
NumberPredicateProvider numMatchProvider;
@Inject
StringPredicateProvider strMatchProvider;
@Inject
SearchService searchService;

String querying

Results results = searchService.search(workspace, new StringQuery(strMatchProvider.newEqualPredicate("Hello world")));
Results results = searchService.search(workspace, new StringQuery(strMatchProvider.newStartsWithPredicate("Hello")));
Results results = searchService.search(workspace, new StringQuery(strMatchProvider.newEndsWithPredicate("world")));

All the available built-in predicates come from StringPredicateProvider, or you can provide your own predicate implementation.

Number querying

Results results = searchService.search(workspace, new NumberQuery(numMatchProvider.newEqualsPredicate(4)));
Results results = searchService.search(workspace, new NumberQuery(numMatchProvider.newAnyOfPredicate(6, 32, 256)));
Results results = searchService.search(workspace, new NumberQuery(numMatchProvider.newRangePredicate(0, 10)));

All the available built-in predicates come from NumberPredicateProvider, or you can provide your own predicate implementation.

Reference querying

Each aspect of a reference (declaring class, name, descriptor) are their own string predicates. You pass null to any of these predicates to match anything for that given aspect. A simple example to find System.out.println() calls would look like:

Results results = searchService.search(workspace, new ReferenceQuery(
         strMatchProvider.newEqualPredicate("java/lang/System"),     // declaring class predicate
         strMatchProvider.newEqualPredicate("out"),                  // reference name predicate
         strMatchProvider.newEqualPredicate("Ljava/io/PrintStream;") // reference descriptor predicate
));

If you want to find all references to a given package you could do something like this:

Results results = searchService.search(workspace, new ReferenceQuery(
         strMatchProvider.newStartsWithPredicate("com/example/"),
         null, // match any field/method name
         null, // match any field/method descriptor
));

Declaration querying

The same ideas from reference querying apply here, but the results are the locations of declared members rather than the locations of references to the members.

Here is an example of a search that would find a FooService.getInput() declaration in a workspace:

Results results = searchService.search(workspace, new DeclarationQuery(
         strMatchProvider.newEqualPredicate("com/example/FooService"),     // declaring class predicate
         strMatchProvider.newEqualPredicate("getInput"),                  // reference name predicate
         strMatchProvider.newEqualPredicate("Ljava/lang/String;") // reference descriptor predicate
));

If you want to find all member declarations that return a specific type you could do something like this:

Results results = searchService.search(workspace, new ReferenceQuery(
         null, // match any class name
         strMatchProvider.newEndsWithPredicate(")Lcom/example/FooService;"),
         null, // match any field/method descriptor
));

Structure querying

The search service also allows you to search classes for structural patterns. This can be useful when you want to find the same class across multiple resources and are sure that that class can be uniquely identified in some way. Here's an example that would find the MultiPlayerGameMode class in a Minecraft client workspace.

// Fields to match:
//  private float destroyProgress;
//  private float destroyTicks;
//  private int destroyDelay;
//  private boolean isDestroying;
FieldMatcher fieldFloat = new FieldMatcherBuilder()
        .type(Type.FLOAT_TYPE)
        .accessFlags(ACC_PRIVATE)
        .build();
FieldMatcher fieldInt = new FieldMatcherBuilder()
        .type(Type.INT_TYPE)
        .accessFlags(ACC_PRIVATE)
        .build();
FieldMatcher fieldBoolean = new FieldMatcherBuilder()
        .type(Type.BOOLEAN_TYPE)
        .accessFlags(ACC_PRIVATE)
        .build();

// Instructions to match in method: public boolean destroyBlock(BlockPos pos, Direction direction)
//    AA:
//        line 263
//        aload this
//        fconst_0
//        putfield net/minecraft/client/multiplayer/MultiPlayerGameMode.destroyProgress F
//    AB:
//        line 264
//        aload this
//        fconst_0
//        putfield net/minecraft/client/multiplayer/MultiPlayerGameMode.destroyTicks F
//    AC:
//        line 265
//        aload this
//        iconst_5
//        putfield net/minecraft/client/multiplayer/MultiPlayerGameMode.destroyDelay I
MethodMatcher resetBreaking = new MethodMatcherBuilder()
        .returnType(Type.BOOLEAN_TYPE)
        .parameters(ListMatcher.anyOfSize(2))
        .accessFlags(ACC_PUBLIC)
        .instructions(List.of(
                // Wildcard to allow any prefix matching
                InsnMatcher.ANY,

                // this.destroyProgress = 0.0f;
                JvmOpcodeInsnMatcher.of(ALOAD),
                JvmOpcodeInsnMatcher.of(FCONST_0),
                JvmOpcodeInsnMatcher.of(PUTFIELD),
                InsnMatcher.ANY, // Intermediate label + line
                // this.destroyTicks = 0.0f;
                JvmOpcodeInsnMatcher.of(ALOAD),
                JvmOpcodeInsnMatcher.of(FCONST_0),
                JvmOpcodeInsnMatcher.of(PUTFIELD),
                InsnMatcher.ANY, // Intermediate label + line
                // this.destroyDelay = 5;
                JvmOpcodeInsnMatcher.of(ALOAD),
                JvmOpcodeInsnMatcher.of(ICONST_5),
                JvmOpcodeInsnMatcher.of(PUTFIELD),

                // Wildcard to allow any suffix matching
                InsnMatcher.ANY
        ), CountConstraint.atLeast(9))
        .build();

// Our class to find should be:
//  - public
//  - have the fields we defined above (destroyProgress, destroyTicks, destroyDelay, isDestroying)
//  - have the method we defined above (destroyBlock)
//  It can have additional fields and methods, but must have at least the ones we defined.
ClassQuery query = new ClassQueryBuilder()
        .accessFlags(ACC_PUBLIC)
        .fields(fieldFloat, fieldFloat, fieldInt, fieldBoolean)
        .methods(resetBreaking)
        .build();

// Should get just one result for 'MultiPlayerGameMode'
Results results = searchService.search(workspace, query);

You can look at the classes in software.coley.recaf.services.search.query.structure to see what sort of matches can be made. The gist is:

  • Class attributes
    • Name
    • Super-name + interfaces
    • Access flags
    • Annotations
  • Fields + their attributes
    • Name
    • Descriptor
    • Access flags
    • Annotations
    • Initial value (for constant fields with a ConstantValue attribute)
  • Methods + their attributes
    • Name
    • Return descriptor
    • Parameter descriptors
    • Access flags
    • Annotations
    • Thrown exceptions
    • Try-catch blocks
    • Instruction patterns

Feedback handler

Passing a feedback handler to the search(...) methods allows you to control what classes and files are searched in by implementing the doVisitClass(ClassInfo) and doVisitFile(FileInfo) methods. Here is a basic example which limits the search to only classes in a given package:

// All methods in the feedback interface default to visit everything, and include all results.
// You can override the 'boolean visitX' methods to control the searching of content within the passed classes/files.
class SkipClassesInPackage implements SearchFeedback {
    private final String pkg;
    
    SkipClassesInPackage(String pkg) { this.pkg = pkg; }
    
    @Override
    public boolean doVisitClass(@Nonnull ClassInfo cls) {
        // Skip if class does not exist in package
        return !cls.getName().startsWith(pkg);
    }
}
SearchFeedback skipping = new SkipClassesInPackage("com/example/");

To control the early abortion of a search you would implement hasRequestedCancellation() to return true after some point. A basic built in class can exists:

// There is a built-in cancellable search implementation.
CancellableSearchFeedback cancellable = new CancellableSearchFeedback();

// Aborts the current search that this feedback is associated with.
cancellable.cancel();

To limit which results are included in the final Results of the search(...) call, implementdoAcceptResult(Result<?>) to return false for results you want to discard. Since the Result contains a PathNode reference to where the match was made at, its probably what you'll want to operate on to implement your own filtering. Here is an example which limits the final Results to include only one item per class:

// This is a silly example, but we really just want to show off how you'd implement this, not how to make a real-world implementation.
class OnlyOneResultPerClass implements SearchFeedback {
    private Set<String> includedClassNames = new HashSet<>();
    
    @Override
    public boolean doAcceptResult(@Nonnull Result<?> result) {
        PathNode<?> pathToValue = result.getPath();
        
        // Get the class value in the path to the value.
        // If the path points to something more specific like a instruction in a method, then
        // this will be the class that defines te method with that instruction in it.
        ClassInfo classPathValue = pathToValue.getValueOfType(ClassInfo.class);
        if (classPathValue != null && !includedClassNames.add(classPathValue.getName())) {
            // If we've already seen a result from this class, skip all the remaining results
            // so that there is only one result per class.
            return false;
        }
        
        // Keep the result in the output
        return true;
    }
}