Skip to content

Commit

Permalink
Allow Workspace.findX to search embedded contents
Browse files Browse the repository at this point in the history
This vastly improves the experience of opening XAPK files and interacting with their contents
  • Loading branch information
Col-E committed Sep 30, 2024
1 parent 96bf4f3 commit 427a873
Show file tree
Hide file tree
Showing 12 changed files with 517 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public int hashCode() {

@Override
public String toString() {
if (properties == null) return "BasicPropertyContainer[0]";
return "BasicPropertyContainer[" + properties.size() + " items]";
String typeName = getClass().getSimpleName();
if (properties == null) return typeName + "[0]";
return typeName + "[" + properties.size() + " items]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.bundle.Bundle;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;

import java.util.Set;
import java.util.function.Consumer;
Expand All @@ -11,6 +13,10 @@
* A <i>"modular"</i> value type for representing <i>"paths"</i> to content in a {@link Workspace}.
* The path must contain all data in a <i>"chain"</i> such that it can have access from most specific portion
* all the way up to the {@link Workspace} portion.
* <p/>
* <b>NOTE: Regarding contents in embedded resources,</b> the path result of the methods like
* {@link Workspace#findClass(String)} will contain the root {@link WorkspaceResource} but the exact {@link Bundle}.
* To find the exact embedded resource of a result use {@link WorkspaceResource#resolveBundleContainer(Bundle)}.
*
* @param <V>
* Path value type.
Expand Down
30 changes: 29 additions & 1 deletion recaf-core/src/main/java/software/coley/recaf/util/Streams.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import software.coley.recaf.util.threading.CountDown;

import java.util.*;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
Expand All @@ -19,11 +26,32 @@ public final class Streams {
private Streams() {
}

/**
* Combines the given streams.
*
* @param streams
* Streams to combine.
* @param <T>
* Stream element type.
*
* @return Combined stream.
*/
public static <T> Stream<? extends T> of(Stream<? extends T>... streams) {
Stream<? extends T> merged = null;
for (Stream<? extends T> stream : streams) {
if (merged == null) merged = stream;
else merged = Stream.concat(merged, stream);
}
return merged == null ? Stream.empty() : merged;
}

/**
* Makes stream interruptable.
*
* @param stream
* Stream to make interruptable.
* @param <T>
* Stream element type.
*
* @return Interruptable stream.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,28 @@
import software.coley.recaf.info.ClassInfo;
import software.coley.recaf.info.FileInfo;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.path.*;
import software.coley.recaf.path.BundlePathNode;
import software.coley.recaf.path.ClassPathNode;
import software.coley.recaf.path.DirectoryPathNode;
import software.coley.recaf.path.FilePathNode;
import software.coley.recaf.path.PathNodes;
import software.coley.recaf.path.ResourcePathNode;
import software.coley.recaf.path.WorkspacePathNode;
import software.coley.recaf.workspace.model.bundle.AndroidClassBundle;
import software.coley.recaf.workspace.model.bundle.Bundle;
import software.coley.recaf.workspace.model.bundle.ClassBundle;
import software.coley.recaf.workspace.model.bundle.FileBundle;
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
import software.coley.recaf.workspace.model.bundle.VersionedJvmClassBundle;
import software.coley.recaf.workspace.model.resource.WorkspaceFileResource;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;

import java.util.*;
import java.util.Comparator;
import java.util.List;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -112,12 +125,9 @@ default List<WorkspaceResource> getAllResources(boolean includeInternal) {
void removeWorkspaceModificationListener(@Nonnull WorkspaceModificationListener listener);

/**
* Searches for a class by the given name in the following bundles:
* <ol>
* <li>The {@link WorkspaceResource#getJvmClassBundle()}</li>
* <li>Each {@link WorkspaceResource#getVersionedJvmClassBundles()}</li>
* <li>Each {@link WorkspaceResource#getAndroidClassBundles()}</li>
* </ol>
* Searches for a class by the given name in the {@link WorkspaceResource#getJvmClassBundle()},
* {@link WorkspaceResource#getVersionedJvmClassBundles()}, and {@link WorkspaceResource#getAndroidClassBundles()}
* of all resources in the workspace <i>(Including embedded resources in other resources)</i>.
*
* @param name
* Class name.
Expand All @@ -135,8 +145,8 @@ default ClassPathNode findClass(@Nonnull String name) {
}

/**
* Searches for a class by the given name in the following bundle:
* {@link WorkspaceResource#getJvmClassBundle()}
* Searches for a class by the given name in the {@link WorkspaceResource#getJvmClassBundle()}
* of all resources in the workspace <i>(Including embedded resources in other resources)</i>.
*
* @param name
* Class name.
Expand All @@ -146,60 +156,64 @@ default ClassPathNode findClass(@Nonnull String name) {
@Nullable
default ClassPathNode findJvmClass(@Nonnull String name) {
for (WorkspaceResource resource : getAllResources(true)) {
JvmClassBundle bundle = resource.getJvmClassBundle();
JvmClassInfo classInfo = bundle.get(name);
if (classInfo != null)
return PathNodes.classPath(this, resource, bundle, classInfo);
Optional<ClassPathNode> path = resource.jvmClassBundleStreamRecursive()
.filter(bundle -> bundle.get(name) != null)
.findFirst()
.map(bundle -> {
JvmClassInfo classInfo = bundle.get(name);
return PathNodes.classPath(this, resource, bundle, classInfo);
});
if (path.isPresent()) return path.get();
}
return null;
}

/**
* Searches for a class by the given name in the following bundle:
* {@link WorkspaceResource#getVersionedJvmClassBundles()}
* Searches for a class by the given name in the {@link WorkspaceResource#getVersionedJvmClassBundles()}
* of all resources in the workspace <i>(Including embedded resources in other resources)</i>.
*
* @param name
* Class name.
*
* @return @return Path to <i>the first</i> versioned JVM class matching the given name.
* If there are multiple versions of the class, the latest is used.
* If there are multiple versions of the class, the highest is used.
*/
@Nullable
default ClassPathNode findLatestVersionedJvmClass(@Nonnull String name) {
return findVersionedJvmClass(name, Integer.MAX_VALUE);
}

/**
* Searches for a class by the given name in the target version bundle within
* {@link WorkspaceResource#getVersionedJvmClassBundles()}.
* Searches for a class by the given name in the target version bundle within the
* {@link WorkspaceResource#getVersionedJvmClassBundles()}of all resources in the workspace
* <i>(Including embedded resources in other resources)</i>.
*
* @param name
* Class name.
* @param version
* Version to look for.
* This value is dropped down to the first available version bundle via {@link NavigableMap#floorEntry(Object)}.
*
* @return Path to <i>the first</i> versioned JVM class matching the given name, supporting the given version.
* @return Path to <i>the highest</i> versioned JVM class matching the given name, supporting the given version.
*/
@Nullable
default ClassPathNode findVersionedJvmClass(@Nonnull String name, int version) {
for (WorkspaceResource resource : getAllResources(false)) {
// Get floor version target.
Map.Entry<Integer, JvmClassBundle> entry = resource.getVersionedJvmClassBundles().floorEntry(version);
if (entry == null) continue;

// Bundle exists, check if the class exists in the path.
JvmClassBundle bundle = entry.getValue();
JvmClassInfo classInfo = bundle.get(name);
if (classInfo != null)
return PathNodes.classPath(this, resource, bundle, classInfo);
Optional<ClassPathNode> path = resource.versionedJvmClassBundleStreamRecursive()
.filter(bundle -> bundle.version() <= version && bundle.get(name) != null)
.sorted(Comparator.comparingInt(VersionedJvmClassBundle::version).thenComparing(VersionedJvmClassBundle::hashCode))
.map(bundle -> {
JvmClassInfo classInfo = bundle.get(name);
return PathNodes.classPath(this, resource, bundle, classInfo);
}).findFirst();
if (path.isPresent()) return path.get();
}
return null;
}

/**
* Searches for a class by the given name in the following bundle:
* {@link WorkspaceResource#getAndroidClassBundles()}
* Searches for a class by the given name in the {@link WorkspaceResource#getAndroidClassBundles()}
* of all resources in the workspace <i>(Including embedded resources in other resources)</i>.
*
* @param name
* Class name.
Expand All @@ -209,11 +223,13 @@ default ClassPathNode findVersionedJvmClass(@Nonnull String name, int version) {
@Nullable
default ClassPathNode findAndroidClass(@Nonnull String name) {
for (WorkspaceResource resource : getAllResources(false)) {
for (AndroidClassBundle bundle : resource.getAndroidClassBundles().values()) {
AndroidClassInfo classInfo = bundle.get(name);
if (classInfo != null)
return PathNodes.classPath(this, resource, bundle, classInfo);
}
Optional<ClassPathNode> path = resource.androidClassBundleStreamRecursive()
.filter(bundle -> bundle.get(name) != null)
.map(bundle -> {
AndroidClassInfo classInfo = bundle.get(name);
return PathNodes.classPath(this, resource, bundle, classInfo);
}).findFirst();
if (path.isPresent()) return path.get();
}
return null;
}
Expand Down Expand Up @@ -280,11 +296,17 @@ default Stream<ClassPathNode> jvmClassesStream() {
WorkspacePathNode workspacePath = PathNodes.workspacePath(this);
return allResourcesStream(true)
.flatMap(resource -> {
JvmClassBundle bundle = resource.getJvmClassBundle();
BundlePathNode bundlePath = workspacePath.child(resource).child(bundle);
return bundle.values()
.stream()
.map(cls -> bundlePath.child(cls.getPackageName()).child(cls));
Stream<ClassPathNode> stream = null;
List<JvmClassBundle> bundles = Stream.concat(resource.jvmClassBundleStreamRecursive(), resource.versionedJvmClassBundleStreamRecursive()).toList();
for (JvmClassBundle bundle : bundles) {
BundlePathNode bundlePath = workspacePath.child(resource).child(bundle);
Stream<ClassPathNode> localStream = bundle.values()
.stream()
.map(cls -> bundlePath.child(cls.getPackageName()).child(cls));
if (stream == null) stream = localStream;
else stream = Stream.concat(stream, localStream);
}
return stream == null ? Stream.empty() : stream;
});
}

Expand All @@ -297,14 +319,14 @@ default Stream<ClassPathNode> androidClassesStream() {
return allResourcesStream(true)
.flatMap(resource -> {
Stream<ClassPathNode> stream = null;
for (AndroidClassBundle bundle : resource.getAndroidClassBundles().values()) {
for (AndroidClassBundle bundle : resource.androidClassBundleStreamRecursive().toList()) {
BundlePathNode bundlePath = workspacePath.child(resource).child(bundle);
Stream<ClassPathNode> classStream = bundle.values()
Stream<ClassPathNode> localStream = bundle.values()
.stream()
.map(cls -> bundlePath.child(cls.getPackageName()).child(cls));
stream = stream == null ? classStream : Stream.concat(stream, classStream);
stream = stream == null ? localStream : Stream.concat(stream, localStream);
}
return stream;
return stream == null ? Stream.empty() : stream;
});
}

Expand All @@ -316,11 +338,15 @@ default Stream<FilePathNode> filesStream() {
WorkspacePathNode workspacePath = PathNodes.workspacePath(this);
return allResourcesStream(true)
.flatMap(resource -> {
FileBundle bundle = resource.getFileBundle();
BundlePathNode bundlePath = workspacePath.child(resource).child(bundle);
return bundle.values()
.stream()
.map(cls -> bundlePath.child(cls.getDirectoryName()).child(cls));
Stream<FilePathNode> stream = null;
for (FileBundle bundle : resource.fileBundleStreamRecursive().toList()) {
BundlePathNode bundlePath = workspacePath.child(resource).child(bundle);
Stream<FilePathNode> localStream = bundle.values()
.stream()
.map(cls -> bundlePath.child(cls.getDirectoryName()).child(cls));
stream = stream == null ? localStream : Stream.concat(stream, localStream);
}
return stream == null ? Stream.empty() : stream;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.annotation.Nonnull;
import software.coley.recaf.info.FileInfo;

import java.util.Objects;

/**
* Basic implementation of a workspace resource sourced from a file.
*
Expand All @@ -17,12 +19,17 @@ public class BasicWorkspaceFileResource extends BasicWorkspaceResource implement
*/
public BasicWorkspaceFileResource(WorkspaceFileResourceBuilder builder) {
super(builder);
this.fileInfo = builder.getFileInfo();
this.fileInfo = Objects.requireNonNull(builder.getFileInfo(), "Cannot construct file resource without an associated file");
}

@Nonnull
@Override
public FileInfo getFileInfo() {
return fileInfo;
}

@Override
public String toString() {
return super.toString() + " " + fileInfo.getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

import java.nio.file.Path;

/**
* Builder for {@link WorkspaceDirectoryResource}.
*
* @author Matt Coley
*/
public class WorkspaceDirectoryResourceBuilder extends WorkspaceResourceBuilder {
private Path directoryPath;

Expand All @@ -23,7 +28,6 @@ public WorkspaceDirectoryResourceBuilder() {
* @param files
* Primary files.
*/

public WorkspaceDirectoryResourceBuilder(JvmClassBundle classes, FileBundle files) {
super(classes, files);
}
Expand Down
Loading

0 comments on commit 427a873

Please sign in to comment.