From 6bf3fdb597dcf7f0064b8f8734dce11f91115af0 Mon Sep 17 00:00:00 2001 From: xdark Date: Fri, 12 Apr 2024 01:52:28 +0300 Subject: [PATCH 1/6] Changes for JDK 22 --- build.gradle | 2 +- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../software/coley/recaf/util/IOUtil.java | 27 ++++-- .../coley/recaf/util/io/ByteSources.java | 6 +- .../recaf/util/io/LocalFileHeaderSource.java | 27 +++--- ...urce.java => MemorySegmentDataSource.java} | 87 +++++++++---------- .../recaf/util/threading/ThreadLocals.java | 25 ------ .../resource/RuntimeWorkspaceResource.java | 12 +-- .../coley/recaf/info/ClassInfoTest.java | 6 +- .../recaf/info/member/FieldMemberTest.java | 18 +++- .../recaf/workspace/PathLoadingManager.java | 2 +- 12 files changed, 106 insertions(+), 110 deletions(-) rename recaf-core/src/main/java/software/coley/recaf/util/io/{ByteDataSource.java => MemorySegmentDataSource.java} (62%) delete mode 100644 recaf-core/src/main/java/software/coley/recaf/util/threading/ThreadLocals.java diff --git a/build.gradle b/build.gradle index 4178dac76..e0fbbbf96 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ subprojects { // gradlew -q javaToolchains - see the list of detected toolchains. java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(22) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac35129b3..bc5f9c40f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ jlinker = "1.0.7" jphantom = "1.4.4" junit = "5.10.2" jsvg = "1.4.0" -llzip = "2.3.0" +llzip = "2.3.0-SNAPSHOT" logback-classic = { strictly = "1.4.11" } # newer releases break in jar releases mapping-io = "0.5.1" mockito = "5.11.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17655d0ef..48c0a02ca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/recaf-core/src/main/java/software/coley/recaf/util/IOUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/IOUtil.java index 912f477af..2b3784199 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/IOUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/IOUtil.java @@ -2,15 +2,28 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import software.coley.recaf.util.threading.ThreadLocals; -import java.io.*; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; @@ -126,7 +139,7 @@ public static byte[] toByteArray(InputStream in, byte[] buf) throws IOException */ @Nonnull public static byte[] toByteArray(InputStream in) throws IOException { - return toByteArray(in, ThreadLocals.getByteBuffer()); + return toByteArray(in, newByteBuffer()); } /** @@ -445,7 +458,7 @@ public static void copy(InputStream in, OutputStream out, byte[] buf) throws IOE * @see IOUtil#newByteBuffer() */ public static void copy(InputStream in, OutputStream out) throws IOException { - copy(in, out, ThreadLocals.getByteBuffer()); + copy(in, out, newByteBuffer()); } /** @@ -616,7 +629,7 @@ public static void copy(URL url, OutputStream out, int connectionTimeoutMillis, int readTimeoutMillis) throws IOException { - copy(url, out, ThreadLocals.getByteBuffer(), connectionTimeoutMillis, readTimeoutMillis); + copy(url, out, newByteBuffer(), connectionTimeoutMillis, readTimeoutMillis); } /** @@ -669,7 +682,7 @@ public static void copy(URL url, Path path, int connectionTimeoutMillis, int readTimeoutMillis) throws IOException { - copy(url, path, ThreadLocals.getByteBuffer(), connectionTimeoutMillis, readTimeoutMillis); + copy(url, path, newByteBuffer(), connectionTimeoutMillis, readTimeoutMillis); } /** diff --git a/recaf-core/src/main/java/software/coley/recaf/util/io/ByteSources.java b/recaf-core/src/main/java/software/coley/recaf/util/io/ByteSources.java index fdcf463ed..7df0a727d 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/io/ByteSources.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/io/ByteSources.java @@ -1,9 +1,9 @@ package software.coley.recaf.util.io; -import software.coley.lljzip.util.ByteData; import software.coley.recaf.util.ReflectUtil; import java.io.IOException; +import java.lang.foreign.MemorySegment; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.function.Consumer; @@ -101,7 +101,7 @@ public static ByteSource forPath(Path path) { * * @return New byte source. */ - public static ByteSource forZip(ByteData data) { - return new ByteDataSource(data); + public static ByteSource forMemorySegment(MemorySegment data) { + return new MemorySegmentDataSource(data); } } diff --git a/recaf-core/src/main/java/software/coley/recaf/util/io/LocalFileHeaderSource.java b/recaf-core/src/main/java/software/coley/recaf/util/io/LocalFileHeaderSource.java index 9d53137a7..19c52c062 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/io/LocalFileHeaderSource.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/io/LocalFileHeaderSource.java @@ -3,11 +3,12 @@ import jakarta.annotation.Nonnull; import software.coley.lljzip.format.compression.ZipCompressions; import software.coley.lljzip.format.model.LocalFileHeader; -import software.coley.lljzip.util.ByteData; -import software.coley.lljzip.util.ByteDataUtil; +import software.coley.lljzip.util.MemorySegmentUtil; import java.io.IOException; import java.io.InputStream; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * Byte source from {@link LocalFileHeader}. @@ -17,7 +18,7 @@ public final class LocalFileHeaderSource implements ByteSource { private final LocalFileHeader fileHeader; private final boolean isAndroid; - private ByteData decompressed; + private MemorySegment decompressed; public LocalFileHeaderSource(LocalFileHeader fileHeader) { this(fileHeader, false); @@ -31,26 +32,24 @@ public LocalFileHeaderSource(LocalFileHeader fileHeader, boolean isAndroid) { @Nonnull @Override public byte[] readAll() throws IOException { - return ByteDataUtil.toByteArray(decompress()); + return MemorySegmentUtil.toByteArray(decompress()); } @Nonnull @Override public byte[] peek(int count) throws IOException { - ByteData data = decompress(); - long length = data.length(); + MemorySegment data = decompress(); + long length = data.byteSize(); if (length < count) count = (int) length; - byte[] bytes = new byte[count]; - data.get(0L, bytes, 0, count); - return bytes; + return data.asSlice(0, count).toArray(ValueLayout.JAVA_BYTE); } @Nonnull @Override public InputStream openStream() throws IOException { // Delegate to byte source - return ByteSources.forZip(decompress()).openStream(); + return ByteSources.forMemorySegment(decompress()).openStream(); } /** @@ -61,12 +60,12 @@ public InputStream openStream() throws IOException { */ public boolean isEmpty() throws IOException { if (fileHeader.getCompressionMethod() == ZipCompressions.STORED) - return fileHeader.getFileData().length() == 0; - return decompress().length() == 0; + return fileHeader.getFileData().byteSize() == 0; + return decompress().byteSize() == 0; } - private ByteData decompress() throws IOException { - ByteData decompressed = this.decompressed; + private MemorySegment decompress() throws IOException { + MemorySegment decompressed = this.decompressed; if (decompressed == null) { // From: https://cs.android.com/android/_/android/platform/frameworks/base/+/b3559643b946829933a76ed45750d13edfefad30:tools/aapt/ZipFile.cpp;l=436 // - If the compression mode given fails, it will get treated as STORED as a fallback diff --git a/recaf-core/src/main/java/software/coley/recaf/util/io/ByteDataSource.java b/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java similarity index 62% rename from recaf-core/src/main/java/software/coley/recaf/util/io/ByteDataSource.java rename to recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java index d5bfbd928..1d2b17ba6 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/io/ByteDataSource.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java @@ -1,69 +1,66 @@ package software.coley.recaf.util.io; import jakarta.annotation.Nonnull; -import software.coley.lljzip.util.ByteData; -import software.coley.lljzip.util.ByteDataUtil; -import software.coley.recaf.util.threading.ThreadLocals; +import software.coley.recaf.util.IOUtil; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** - * {@link ByteSource} implemented via {@link ByteData} from LLJ-zip. + * {@link ByteSource} implemented via {@link MemorySegment}. * * @author xDark */ -public final class ByteDataSource implements ByteSource, AutoCloseable { - private final ByteData data; +public final class MemorySegmentDataSource implements ByteSource, AutoCloseable { + private final MemorySegment data; /** * @param data * Data to read bytes from. */ - public ByteDataSource(ByteData data) { + public MemorySegmentDataSource(MemorySegment data) { this.data = data; } @Override public void close() throws Exception { - data.close(); } @Nonnull @Override public byte[] readAll() throws IOException { - ByteData data = this.data; - if (data.length() > Integer.MAX_VALUE - 8) { + MemorySegment data = this.data; + if (data.byteSize() > Integer.MAX_VALUE - 8) { throw new IOException("Too large content"); } - return ByteDataUtil.toByteArray(data); + return data.toArray(ValueLayout.JAVA_BYTE); } @Nonnull @Override public byte[] peek(int count) { - ByteData data = this.data; - count = (int) Math.min(count, data.length()); - byte[] buf = new byte[count]; - data.get(0L, buf, 0, count); - return buf; + MemorySegment data = this.data; + count = (int) Math.min(count, data.byteSize()); + return data.asSlice(0, count).toArray(ValueLayout.JAVA_BYTE); } @Nonnull @Override public InputStream openStream() { - return new ByteDataInputStream(data); + return new MemorySegmentInputStream(data); } - private static final class ByteDataInputStream extends InputStream { - private final ByteData data; + private static final class MemorySegmentInputStream extends InputStream { + private final MemorySegment data; private long read; private long markedOffset = -1; private long markedLimit; private volatile boolean closed; - ByteDataInputStream(ByteData data) { + MemorySegmentInputStream(MemorySegment data) { this.data = data; } @@ -98,27 +95,27 @@ public synchronized void reset() { @Override public int read() throws IOException { ensureOpen(); - ByteData data = this.data; - if (read >= data.length()) { + MemorySegment data = this.data; + if (read >= data.byteSize()) { return -1; } - byte b = data.get(read++); + byte b = data.get(ValueLayout.JAVA_BYTE, read++); checkMarkLimit(); - return b; + return b & 0xff; } @Override public int read(@Nonnull byte[] b, int off, int len) throws IOException { ensureOpen(); - ByteData data = this.data; + MemorySegment data = this.data; long read = this.read; - long length = data.length(); + long length = data.byteSize(); if (read >= length) { return -1; } long remaining = length - read; len = (int) Math.min(remaining, len); - data.get(read, b, off, len); + MemorySegment.copy(data, read, MemorySegment.ofArray(b), off, len); this.read += len; checkMarkLimit(); return len; @@ -127,16 +124,16 @@ public int read(@Nonnull byte[] b, int off, int len) throws IOException { @Override public byte[] readNBytes(int len) throws IOException { ensureOpen(); - ByteData data = this.data; + MemorySegment data = this.data; long read = this.read; - long length = data.length(); + long length = data.byteSize(); if (read >= length) { return new byte[0]; } long remaining = length - read; len = (int) Math.min(remaining, len); byte[] buf = new byte[len]; - data.get(read, buf, 0, len); + MemorySegment.copy(data, read, MemorySegment.ofArray(buf), 0, len); this.read += len; checkMarkLimit(); return buf; @@ -145,9 +142,9 @@ public byte[] readNBytes(int len) throws IOException { @Override public long skip(long n) throws IOException { ensureOpen(); - ByteData data = this.data; + MemorySegment data = this.data; long read = this.read; - long length = data.length(); + long length = data.byteSize(); if (read >= length) { return 0; } @@ -160,8 +157,8 @@ public long skip(long n) throws IOException { @Override public int available() throws IOException { ensureOpen(); - ByteData data = this.data; - long length = data.length(); + MemorySegment data = this.data; + long length = data.byteSize(); long read = this.read; if (read >= length) { return 0; @@ -174,27 +171,27 @@ public int available() throws IOException { @Override public void close() throws IOException { - if (!closed) { - synchronized (this) { - if (closed) - return; - closed = true; - data.close(); - } - } + closed = true; } @Override public long transferTo(OutputStream out) throws IOException { ensureOpen(); - ByteData data = this.data; - long length = data.length(); + MemorySegment data = this.data; + long length = data.byteSize(); long read = this.read; if (read >= length) { return 0L; } long remaining = length - read; - data.transferTo(out, ThreadLocals.getByteBuffer()); + byte[] buffer = IOUtil.newByteBuffer(); + MemorySegment bufferSegment = MemorySegment.ofArray(buffer); + while (read < length) { + int copyable = (int) Math.min(buffer.length, length - read); + MemorySegment.copy(data, read, bufferSegment, 0, copyable); + out.write(buffer, 0, copyable); + read += copyable; + } this.read = length; checkMarkLimit(); return remaining; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/threading/ThreadLocals.java b/recaf-core/src/main/java/software/coley/recaf/util/threading/ThreadLocals.java deleted file mode 100644 index bf968ad9d..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/threading/ThreadLocals.java +++ /dev/null @@ -1,25 +0,0 @@ -package software.coley.recaf.util.threading; - -import software.coley.recaf.util.IOUtil; - -/** - * Some useful {@link ThreadLocal}s. - * - * @author xDark - */ -public final class ThreadLocals { - private static final ThreadLocal BYTE_BUFFER = ThreadLocal.withInitial(IOUtil::newByteBuffer); - - /** - * Deny all constructions. - */ - private ThreadLocals() { - } - - /** - * @return Thread-local byte buffer. - */ - public static byte[] getByteBuffer() { - return BYTE_BUFFER.get(); - } -} diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java index 09301a597..0c15dd39e 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java @@ -7,8 +7,11 @@ import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.builder.JvmClassInfoBuilder; import software.coley.recaf.util.IOUtil; -import software.coley.recaf.util.threading.ThreadLocals; -import software.coley.recaf.workspace.model.bundle.*; +import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; +import software.coley.recaf.workspace.model.bundle.BasicFileBundle; +import software.coley.recaf.workspace.model.bundle.BasicJvmClassBundle; +import software.coley.recaf.workspace.model.bundle.FileBundle; +import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import java.io.IOException; import java.io.InputStream; @@ -42,12 +45,9 @@ public static RuntimeWorkspaceResource getInstance() { } private RuntimeWorkspaceResource() { - RuntimeWorkspaceResource resource = this; classes = new BasicJvmClassBundle() { @Override public JvmClassInfo get(@Nonnull Object name) { - if (name == null) - return null; String key = name.toString(); if (key.indexOf('.') >= 0) key = key.replace('.', '/'); @@ -60,7 +60,7 @@ public JvmClassInfo get(@Nonnull Object name) { byte[] value = null; try (InputStream in = ClassLoader.getSystemResourceAsStream(key + ".class")) { if (in != null) { - value = IOUtil.toByteArray(in, ThreadLocals.getByteBuffer()); + value = IOUtil.toByteArray(in); } } catch (IOException ex) { logger.error("Failed to fetch runtime bytecode of class: " + key, ex); diff --git a/recaf-core/src/test/java/software/coley/recaf/info/ClassInfoTest.java b/recaf-core/src/test/java/software/coley/recaf/info/ClassInfoTest.java index c50db0b03..a3ac16e11 100644 --- a/recaf-core/src/test/java/software/coley/recaf/info/ClassInfoTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/info/ClassInfoTest.java @@ -253,7 +253,8 @@ void getFields() { assertEquals(5, accessibleFields.getFields().size()); // Non-static inner should have a reference to outer as synthetic field - assertEquals(1, classWithInner$Inner.getFields().size()); + // Not anymore! :V + // assertEquals(1, classWithInner$Inner.getFields().size()); // Despite how the format looks, they're not fields assertEquals(0, annotationImpl.getFields().size()); @@ -272,7 +273,8 @@ void getMethods() { void fieldStream() { // Mirror results of prior tests assertEquals(5, accessibleFields.fieldStream().count()); - assertEquals(1, classWithInner$Inner.fieldStream().count()); + // See comment in getFields. + // assertEquals(1, classWithInner$Inner.fieldStream().count()); assertEquals(0, annotationImpl.fieldStream().count()); } diff --git a/recaf-core/src/test/java/software/coley/recaf/info/member/FieldMemberTest.java b/recaf-core/src/test/java/software/coley/recaf/info/member/FieldMemberTest.java index f11b8faf8..8f2200b06 100644 --- a/recaf-core/src/test/java/software/coley/recaf/info/member/FieldMemberTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/info/member/FieldMemberTest.java @@ -1,5 +1,6 @@ package software.coley.recaf.info.member; +import jakarta.annotation.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import software.coley.recaf.info.JvmClassInfo; @@ -30,6 +31,7 @@ class FieldMemberTest { static FieldMember var_volatileField; static FieldMember var_transientField; static JvmClassInfo classWithInner$Inner; + @Nullable static FieldMember inner_outerRefField; @@ -49,7 +51,9 @@ static void setup() throws IOException { var_transientField = variousFields.getDeclaredField("transientField", "I"); classWithInner$Inner = TestClassUtils.fromRuntimeClass(ClassWithInner.TheInner.class); - inner_outerRefField = classWithInner$Inner.getFields().get(0); + if (!classWithInner$Inner.getFields().isEmpty()) { + inner_outerRefField = classWithInner$Inner.getFields().get(0); + } } @Test @@ -203,7 +207,9 @@ void hasVarargsModifier() { @Test void hasBridgeModifier() { // Bridge is for methods, only similar mod is synthetic, but they are not the same. - assertFalse(inner_outerRefField.hasBridgeModifier()); + if (inner_outerRefField != null) { + assertFalse(inner_outerRefField.hasBridgeModifier()); + } for (FieldMember field : acc_fields) { assertFalse(field.hasBridgeModifier()); } @@ -214,7 +220,9 @@ void hasSyntheticModifier() { for (FieldMember field : acc_fields) { assertFalse(field.hasSyntheticModifier()); } - assertTrue(inner_outerRefField.hasSyntheticModifier()); + if (inner_outerRefField != null) { + assertTrue(inner_outerRefField.hasSyntheticModifier()); + } } @Test @@ -223,7 +231,9 @@ void isCompilerGenerated() { for (FieldMember field : acc_fields) { assertFalse(field.isCompilerGenerated()); } - assertTrue(inner_outerRefField.isCompilerGenerated()); + if (inner_outerRefField != null) { + assertTrue(inner_outerRefField.isCompilerGenerated()); + } } @Test diff --git a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java index 6a7d9a115..07042256b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java @@ -78,7 +78,7 @@ public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List sup // Wrap into workspace and assign it Workspace workspace = new BasicWorkspace(primaryResource, supportingResources); - workspaceManager.setCurrent(workspace); + workspaceManager.setCurrent(workspace); } catch (Throwable t) { errorHandling.accept(t); } From a0d2b3f3e9337b21336f4ca752a9aba6a2a985e9 Mon Sep 17 00:00:00 2001 From: xdark Date: Fri, 12 Apr 2024 01:53:22 +0300 Subject: [PATCH 2/6] Undo tab indent --- .../java/software/coley/recaf/workspace/PathLoadingManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java index 07042256b..6a7d9a115 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java @@ -78,7 +78,7 @@ public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List sup // Wrap into workspace and assign it Workspace workspace = new BasicWorkspace(primaryResource, supportingResources); - workspaceManager.setCurrent(workspace); + workspaceManager.setCurrent(workspace); } catch (Throwable t) { errorHandling.accept(t); } From f445cefa72b7570b1db4ee99d0eaa47e11419846 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 26 Apr 2024 22:56:45 -0400 Subject: [PATCH 3/6] Update LLJZip & use nightly gradle to support JDK 22 --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bc5f9c40f..b9c34bdae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ jlinker = "1.0.7" jphantom = "1.4.4" junit = "5.10.2" jsvg = "1.4.0" -llzip = "2.3.0-SNAPSHOT" +llzip = "2.5.0" logback-classic = { strictly = "1.4.11" } # newer releases break in jar releases mapping-io = "0.5.1" mockito = "5.11.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48c0a02ca..7bc51a15a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https://services.gradle.org/distributions-snapshots/gradle-8.9-20240426001649+0000-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 0d632a8b4b0af265f001dc1c166b484ebf695a7a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 26 Apr 2024 23:54:34 -0400 Subject: [PATCH 4/6] Use JDK 22 in CI build --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99c9e7109..09d62e7b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - java-version: [ 17 ] + java-version: [ 22 ] runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -33,7 +33,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 22 check-latest: true # The project version extract NEEDS to have the gradle wrapper already downloaded. # So we have a dummy step here just to initialize it. @@ -107,7 +107,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 22 # The project version extract NEEDS to have the gradle wrapper already downloaded. # So we have a dummy step here just to initialize it. - name: Download Gradle wrapper From d92a5435cf33305fe28be7528c4c0aabcae39614 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 27 Apr 2024 00:21:52 -0400 Subject: [PATCH 5/6] Address odd edge case where some discovered 'zip' files aren't zips. Mostly useful for not killing workspace reading when looking at embedded files. --- .../services/workspace/io/BasicResourceImporter.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java index 02c041f79..a44e8f780 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java @@ -125,9 +125,17 @@ private WorkspaceFileResource handleZip(WorkspaceFileResourceBuilder builder, Zi NavigableMap versionedJvmClassBundles = new TreeMap<>(); Map embeddedResources = new HashMap<>(); - // Read ZIP entries + // Read ZIP boolean isAndroid = zipInfo.getName().toLowerCase().endsWith(".apk"); ZipArchive archive = config.mapping().apply(source.readAll()); + + // Sanity check, if there's data at the head of the file AND its otherwise empty its probably junk. + if (archive.getPrefixData() != null && archive.getEnd() != null && archive.getParts().size() == 1) { + // We'll throw as the caller should catch this case and handle it based on their needs. + throw new IOException("Content matched ZIP header but had no file entries"); + } + + // Build model from the contained files in the ZIP archive.getLocalFiles().forEach(header -> { LocalFileHeaderSource headerSource = new LocalFileHeaderSource(header, isAndroid); String entryName = header.getFileNameAsString(); From 4a6cc16023174bb9e9096b538894d0700a56a817 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 27 Apr 2024 00:38:15 -0400 Subject: [PATCH 6/6] Support Jar file data prefixes when exporting For cases like Jar2Exe --- .../builtin/ZipPrefixDataProperty.java | 58 +++++++++++++++++++ .../workspace/io/BasicResourceImporter.java | 10 +++- .../workspace/io/WorkspaceExportOptions.java | 20 ++++++- 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ZipPrefixDataProperty.java diff --git a/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ZipPrefixDataProperty.java b/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ZipPrefixDataProperty.java new file mode 100644 index 000000000..5ec9eae65 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ZipPrefixDataProperty.java @@ -0,0 +1,58 @@ +package software.coley.recaf.info.properties.builtin; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import software.coley.recaf.info.Info; +import software.coley.recaf.info.properties.BasicProperty; +import software.coley.recaf.info.properties.Property; + +/** + * Built in property to track data appearing before the ZIP header in an archive. + * + * @author Matt Coley + */ +public class ZipPrefixDataProperty extends BasicProperty { + public static final String KEY = "zip-prefix-data"; + + /** + * @param data + * Optional data. + */ + public ZipPrefixDataProperty(@Nullable byte[] data) { + super(KEY, data); + } + + /** + * @param info + * Info instance. + * + * @return Optional data. + * {@code null} when no property value is assigned. + */ + @Nullable + public static byte[] get(@Nonnull Info info) { + Property property = info.getProperty(KEY); + if (property != null) { + return property.value(); + } + return null; + } + + /** + * @param info + * Info instance. + * @param value + * Optional data. + */ + public static void set(@Nonnull Info info, @Nonnull byte[] value) { + info.setProperty(new ZipPrefixDataProperty(value)); + } + + /** + * @param info + * Info instance. + */ + public static void remove(@Nonnull Info info) { + info.removeProperty(KEY); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java index a44e8f780..075790f27 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java @@ -7,6 +7,7 @@ import software.coley.lljzip.format.model.CentralDirectoryFileHeader; import software.coley.lljzip.format.model.ZipArchive; import software.coley.lljzip.util.ExtraFieldTime; +import software.coley.lljzip.util.MemorySegmentUtil; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.*; import software.coley.recaf.info.builder.FileInfoBuilder; @@ -21,6 +22,7 @@ import java.io.File; import java.io.IOException; +import java.lang.foreign.MemorySegment; import java.net.URL; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; @@ -130,11 +132,17 @@ private WorkspaceFileResource handleZip(WorkspaceFileResourceBuilder builder, Zi ZipArchive archive = config.mapping().apply(source.readAll()); // Sanity check, if there's data at the head of the file AND its otherwise empty its probably junk. - if (archive.getPrefixData() != null && archive.getEnd() != null && archive.getParts().size() == 1) { + MemorySegment prefixData = archive.getPrefixData(); + if (prefixData != null && archive.getEnd() != null && archive.getParts().size() == 1) { // We'll throw as the caller should catch this case and handle it based on their needs. throw new IOException("Content matched ZIP header but had no file entries"); } + // Record prefix data to attribute held by the zip file info. + if (prefixData != null) { + ZipPrefixDataProperty.set(zipInfo, MemorySegmentUtil.toByteArray(prefixData)); + } + // Build model from the contained files in the ZIP archive.getLocalFiles().forEach(header -> { LocalFileHeaderSource headerSource = new LocalFileHeaderSource(header, isAndroid); diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java index c688f4ceb..6b87cef1c 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java @@ -3,9 +3,9 @@ import jakarta.annotation.Nonnull; import software.coley.recaf.info.*; import software.coley.recaf.info.properties.builtin.*; +import software.coley.recaf.services.workspace.WorkspaceManager; import software.coley.recaf.util.Unchecked; import software.coley.recaf.util.ZipCreationUtils; -import software.coley.recaf.services.workspace.WorkspaceManager; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; @@ -18,6 +18,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -136,6 +137,7 @@ private class WorkspaceExporterImpl implements WorkspaceExporter { private final Map modifyTimes = new HashMap<>(); private final Map createTimes = new HashMap<>(); private final Map accessTimes = new HashMap<>(); + private byte[] prefix; @Override public void export(@Nonnull Workspace workspace) throws IOException { @@ -163,7 +165,12 @@ public void export(@Nonnull Workspace workspace) throws IOException { }); // Write buffer to path - Files.write(path, zipBuilder.bytes()); + if (prefix != null) { + Files.write(path, prefix); + Files.write(path, zipBuilder.bytes(), StandardOpenOption.APPEND); + } else { + Files.write(path, zipBuilder.bytes()); + } break; case DIRECTORY: for (Map.Entry entry : contents.entrySet()) { @@ -186,12 +193,19 @@ public void export(@Nonnull Workspace workspace) throws IOException { * Workspace to pull data from. */ private void populate(@Nonnull Workspace workspace) { + // If shading libs, they go first so the primary content will be the authoritative copy for + // any duplicate paths held by both resources. if (bundleSupporting) { for (WorkspaceResource supportingResource : workspace.getSupportingResources()) { mapInto(contents, supportingResource); } } - mapInto(contents, workspace.getPrimaryResource()); + WorkspaceResource primary = workspace.getPrimaryResource(); + mapInto(contents, primary); + + // If the resource had prefix data, get it here so that we can write it back later. + if (primary instanceof WorkspaceFileResource resource) + prefix = ZipPrefixDataProperty.get(resource.getFileInfo()); } /**