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()); } /**