mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Dicing things up a little
- Trying to slim this beauty down to a size 6. - Shes a size 7.1
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.FieldVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -6,8 +13,8 @@ import java.util.Set;
|
||||
|
||||
/**
|
||||
* Reads the {@code CONSTANT_Class} entries of a class file. That is exactly the set of types the
|
||||
* class links against - supertypes, field and method owners, casts, catches, constant class
|
||||
* literals. Annotation types live in the annotation attributes as plain UTF-8 descriptors and are
|
||||
* class names directly - supertypes, field and method owners, casts, catches, constant class
|
||||
* literals. Field and method descriptors are also traversed to include signature-only types. Annotation types live in the annotation attributes as plain UTF-8 descriptors and are
|
||||
* deliberately not reported: the JVM skips an annotation whose type is absent.
|
||||
*/
|
||||
public final class ClassReferences {
|
||||
@@ -42,6 +49,7 @@ public final class ClassReferences {
|
||||
int constantCount = readUnsignedShort(classFile, 8);
|
||||
String[] utf8 = new String[constantCount];
|
||||
List<Integer> classNameIndexes = new ArrayList<>();
|
||||
List<Integer> descriptorIndexes = new ArrayList<>();
|
||||
int offset = 10;
|
||||
int index = 1;
|
||||
while (index < constantCount) {
|
||||
@@ -58,10 +66,18 @@ public final class ClassReferences {
|
||||
classNameIndexes.add(readUnsignedShort(classFile, offset));
|
||||
offset += 2;
|
||||
}
|
||||
case CONSTANT_STRING, CONSTANT_METHOD_TYPE, CONSTANT_MODULE, CONSTANT_PACKAGE -> offset += 2;
|
||||
case CONSTANT_METHOD_TYPE -> {
|
||||
descriptorIndexes.add(readUnsignedShort(classFile, offset));
|
||||
offset += 2;
|
||||
}
|
||||
case CONSTANT_NAME_AND_TYPE -> {
|
||||
descriptorIndexes.add(readUnsignedShort(classFile, offset + 2));
|
||||
offset += 4;
|
||||
}
|
||||
case CONSTANT_STRING, CONSTANT_MODULE, CONSTANT_PACKAGE -> offset += 2;
|
||||
case CONSTANT_METHOD_HANDLE -> offset += 3;
|
||||
case CONSTANT_INTEGER, CONSTANT_FLOAT, CONSTANT_FIELDREF, CONSTANT_METHODREF,
|
||||
CONSTANT_INTERFACE_METHODREF, CONSTANT_NAME_AND_TYPE, CONSTANT_DYNAMIC,
|
||||
CONSTANT_INTERFACE_METHODREF, CONSTANT_DYNAMIC,
|
||||
CONSTANT_INVOKE_DYNAMIC -> offset += 4;
|
||||
case CONSTANT_LONG, CONSTANT_DOUBLE -> {
|
||||
offset += 8;
|
||||
@@ -83,9 +99,41 @@ public final class ClassReferences {
|
||||
references.add(name);
|
||||
}
|
||||
}
|
||||
for (int descriptorIndex : descriptorIndexes) {
|
||||
addType(Type.getType(utf8[descriptorIndex]), references);
|
||||
}
|
||||
new ClassReader(classFile).accept(new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
|
||||
addType(Type.getType(descriptor), references);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
|
||||
String[] exceptions) {
|
||||
addType(Type.getMethodType(descriptor), references);
|
||||
return null;
|
||||
}
|
||||
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
|
||||
return references;
|
||||
}
|
||||
|
||||
private static void addType(Type type, Set<String> references) {
|
||||
switch (type.getSort()) {
|
||||
case Type.ARRAY -> addType(type.getElementType(), references);
|
||||
case Type.OBJECT -> references.add(type.getInternalName());
|
||||
case Type.METHOD -> {
|
||||
addType(type.getReturnType(), references);
|
||||
for (Type argument : type.getArgumentTypes()) {
|
||||
addType(argument, references);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String rawName) {
|
||||
if (rawName == null || rawName.isEmpty()) {
|
||||
return null;
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import org.apache.commons.compress.archivers.zip.ExtraFieldUtils;
|
||||
import org.apache.commons.compress.archivers.zip.Zip64Mode;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||
import org.gradle.api.GradleException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public final class JarCompactor {
|
||||
private static final int BUFFER_BYTES = 64 * 1024;
|
||||
|
||||
private JarCompactor() {
|
||||
}
|
||||
|
||||
public static void compact(File artifact) {
|
||||
if (artifact == null || !artifact.isFile()) {
|
||||
throw new GradleException("Cannot compact missing jar artifact: " + artifact);
|
||||
}
|
||||
|
||||
Path source = artifact.toPath();
|
||||
Path temporary = null;
|
||||
try {
|
||||
temporary = Files.createTempFile(source.getParent(), artifact.getName(), ".compact");
|
||||
rewrite(source, temporary);
|
||||
replace(temporary, source);
|
||||
} catch (IOException exception) {
|
||||
if (temporary != null) {
|
||||
try {
|
||||
Files.deleteIfExists(temporary);
|
||||
} catch (IOException cleanupFailure) {
|
||||
exception.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
throw new GradleException("Unable to compact jar artifact " + artifact.getAbsolutePath(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void rewrite(Path source, Path destination) throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_BYTES];
|
||||
try (ZipFile input = new ZipFile(source.toFile());
|
||||
ZipArchiveOutputStream output = new ZipArchiveOutputStream(destination)) {
|
||||
if (!output.isSeekable()) {
|
||||
throw new IOException("Jar compaction requires seekable output.");
|
||||
}
|
||||
output.setLevel(9);
|
||||
output.setUseZip64(Zip64Mode.Never);
|
||||
output.setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER);
|
||||
Set<String> names = new HashSet<>();
|
||||
Enumeration<? extends ZipEntry> entries = input.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (!names.add(entry.getName())) {
|
||||
throw new IOException("Duplicate jar entry: " + entry.getName());
|
||||
}
|
||||
output.putArchiveEntry(copyMetadata(entry));
|
||||
copyEntry(input, output, entry, buffer);
|
||||
output.closeArchiveEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyEntry(ZipFile input, ZipArchiveOutputStream output, ZipEntry entry, byte[] buffer)
|
||||
throws IOException {
|
||||
CRC32 crc = new CRC32();
|
||||
long size = 0L;
|
||||
try (InputStream content = input.getInputStream(entry)) {
|
||||
int read;
|
||||
while ((read = content.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, read);
|
||||
crc.update(buffer, 0, read);
|
||||
size += read;
|
||||
}
|
||||
}
|
||||
if (size != entry.getSize() || crc.getValue() != entry.getCrc()) {
|
||||
throw new IOException("Jar entry failed size or CRC verification: " + entry.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static ZipArchiveEntry copyMetadata(ZipEntry source) throws IOException {
|
||||
ZipArchiveEntry target = new ZipArchiveEntry(source.getName());
|
||||
target.setMethod(ZipEntry.DEFLATED);
|
||||
if (source.getTime() >= 0L) {
|
||||
target.setTime(source.getTime());
|
||||
}
|
||||
if (source.getComment() != null) {
|
||||
target.setComment(source.getComment());
|
||||
}
|
||||
if (source.getExtra() != null) {
|
||||
target.setExtraFields(ExtraFieldUtils.parse(source.getExtra(), true,
|
||||
ZipArchiveEntry.ExtraFieldParsingMode.BEST_EFFORT));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static void replace(Path temporary, Path source) throws IOException {
|
||||
try {
|
||||
Files.move(
|
||||
temporary,
|
||||
source,
|
||||
StandardCopyOption.ATOMIC_MOVE,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(temporary, source, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import org.junit.Test;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ClassReferencesTest {
|
||||
@Test
|
||||
public void includesDeclarationOnlyFieldAndMethodTypes() {
|
||||
ClassWriter writer = createClass();
|
||||
writer.visitField(Opcodes.ACC_PUBLIC, "field", "[[Lexample/FieldType;", null, null).visitEnd();
|
||||
writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, "method",
|
||||
"(Lexample/Argument;[I)[Lexample/Result;", null, null).visitEnd();
|
||||
writer.visitEnd();
|
||||
|
||||
Set<String> references = ClassReferences.read(writer.toByteArray());
|
||||
assertTrue(references.containsAll(Set.of("example/FieldType", "example/Argument", "example/Result")));
|
||||
assertFalse(references.contains("I"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void includesMemberReferenceAndMethodTypeDescriptors() {
|
||||
ClassWriter writer = createClass();
|
||||
writer.newMethod("example/Owner", "method", "(Lexample/Argument;)Lexample/Result;", false);
|
||||
writer.newConst(Type.getMethodType("(Lexample/HandleArgument;)Lexample/HandleResult;"));
|
||||
writer.visitEnd();
|
||||
|
||||
Set<String> references = ClassReferences.read(writer.toByteArray());
|
||||
assertTrue(references.containsAll(Set.of("example/Owner", "example/Argument", "example/Result",
|
||||
"example/HandleArgument", "example/HandleResult")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotTreatStringsOrAnnotationOnlyTypesAsLinkage() {
|
||||
ClassWriter writer = createClass();
|
||||
writer.newConst("Lexample/StringValue;");
|
||||
writer.visitAnnotation("Lexample/Annotation;", true).visitEnd();
|
||||
writer.visitEnd();
|
||||
|
||||
Set<String> references = ClassReferences.read(writer.toByteArray());
|
||||
assertFalse(references.contains("example/StringValue"));
|
||||
assertFalse(references.contains("example/Annotation"));
|
||||
}
|
||||
|
||||
private static ClassWriter createClass() {
|
||||
ClassWriter writer = new ClassWriter(0);
|
||||
writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT,
|
||||
"example/Consumer", null, "java/lang/Object", null);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class JarCompactorTest {
|
||||
@Rule
|
||||
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void preservesFilesAndOmitsDirectoryEntries() throws Exception {
|
||||
File artifact = temporaryFolder.newFile("artifact.jar");
|
||||
byte[] content = "Iris artifact content".getBytes(StandardCharsets.UTF_8);
|
||||
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
|
||||
output.putNextEntry(new JarEntry("example/"));
|
||||
output.closeEntry();
|
||||
output.putNextEntry(new JarEntry("example/value.txt"));
|
||||
output.write(content);
|
||||
output.closeEntry();
|
||||
}
|
||||
|
||||
JarCompactor.compact(artifact);
|
||||
|
||||
try (JarFile jar = new JarFile(artifact)) {
|
||||
assertNull(jar.getJarEntry("example/"));
|
||||
assertArrayEquals(content, jar.getInputStream(
|
||||
jar.getJarEntry("example/value.txt")).readAllBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesJarIdentityAndProducesDeterministicSeekableOutput() throws Exception {
|
||||
File artifact = temporaryFolder.newFile("identity.jar");
|
||||
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact));
|
||||
InputStream classResource = getClass().getResourceAsStream("/JarCompactorTest.class")) {
|
||||
output.setLevel(9);
|
||||
writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n"
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes());
|
||||
writeEntry(output, "META-INF/iris/generation-kernels/abi-1.revision", "registry fixture".getBytes(StandardCharsets.UTF_8));
|
||||
writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8));
|
||||
for (int index = 0; index < 100; index++) {
|
||||
writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
Map<String, EntryIdentity> original = identity(artifact);
|
||||
long originalBytes = artifact.length();
|
||||
|
||||
JarCompactor.compact(artifact);
|
||||
|
||||
assertEquals(original, identity(artifact));
|
||||
assertTrue(artifact.length() < originalBytes);
|
||||
try (JarFile jar = new JarFile(artifact)) {
|
||||
assertEquals("true", jar.getManifest().getMainAttributes().getValue("Multi-Release"));
|
||||
assertEquals(0xCA, jar.getInputStream(jar.getJarEntry("JarCompactorTest.class")).read());
|
||||
}
|
||||
try (ZipFile zip = ZipFile.builder().setFile(artifact).get()) {
|
||||
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
assertFalse(entries.nextElement().getGeneralPurposeBit().usesDataDescriptor());
|
||||
}
|
||||
}
|
||||
byte[] first = Files.readAllBytes(artifact.toPath());
|
||||
JarCompactor.compact(artifact);
|
||||
assertArrayEquals(first, Files.readAllBytes(artifact.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedArchivesWithoutReplacingTheSource() throws Exception {
|
||||
File artifact = temporaryFolder.newFile("invalid.jar");
|
||||
byte[] original = "not a zip archive".getBytes(StandardCharsets.UTF_8);
|
||||
Files.write(artifact.toPath(), original);
|
||||
|
||||
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
|
||||
|
||||
assertArrayEquals(original, Files.readAllBytes(artifact.toPath()));
|
||||
assertEquals(1, temporaryFolder.getRoot().listFiles().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsCorruptPayloadWithoutReplacingTheSource() throws Exception {
|
||||
File artifact = temporaryFolder.newFile("corrupt.jar");
|
||||
byte[] payload = "checked-payload".getBytes(StandardCharsets.UTF_8);
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(payload);
|
||||
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
|
||||
JarEntry entry = new JarEntry("value.txt");
|
||||
entry.setMethod(ZipEntry.STORED);
|
||||
entry.setSize(payload.length);
|
||||
entry.setCrc(crc.getValue());
|
||||
output.putNextEntry(entry);
|
||||
output.write(payload);
|
||||
output.closeEntry();
|
||||
}
|
||||
byte[] corrupt = Files.readAllBytes(artifact.toPath());
|
||||
int payloadOffset = new String(corrupt, StandardCharsets.ISO_8859_1).indexOf("checked-payload");
|
||||
assertTrue(payloadOffset > 0);
|
||||
corrupt[payloadOffset] ^= 1;
|
||||
Files.write(artifact.toPath(), corrupt);
|
||||
|
||||
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
|
||||
|
||||
assertArrayEquals(corrupt, Files.readAllBytes(artifact.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsDuplicateEntriesWithoutReplacingTheSource() throws Exception {
|
||||
File artifact = temporaryFolder.newFile("duplicate.jar");
|
||||
try (ZipArchiveOutputStream output = new ZipArchiveOutputStream(artifact)) {
|
||||
for (int index = 0; index < 2; index++) {
|
||||
output.putArchiveEntry(new ZipArchiveEntry("duplicate.txt"));
|
||||
output.write(index);
|
||||
output.closeArchiveEntry();
|
||||
}
|
||||
}
|
||||
byte[] original = Files.readAllBytes(artifact.toPath());
|
||||
|
||||
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
|
||||
|
||||
assertArrayEquals(original, Files.readAllBytes(artifact.toPath()));
|
||||
}
|
||||
|
||||
private static void writeEntry(JarOutputStream output, String name, byte[] content) throws Exception {
|
||||
JarEntry entry = new JarEntry(name);
|
||||
entry.setTime(315_532_800_000L);
|
||||
entry.setComment("preserved entry comment");
|
||||
entry.setExtra(new byte[]{(byte) 0xFE, (byte) 0xCA, 1, 0, 7});
|
||||
output.putNextEntry(entry);
|
||||
output.write(content);
|
||||
output.closeEntry();
|
||||
}
|
||||
|
||||
private static Map<String, EntryIdentity> identity(File artifact) throws Exception {
|
||||
Map<String, EntryIdentity> entries = new TreeMap<>();
|
||||
try (JarFile jar = new JarFile(artifact)) {
|
||||
Enumeration<JarEntry> contents = jar.entries();
|
||||
while (contents.hasMoreElements()) {
|
||||
JarEntry entry = contents.nextElement();
|
||||
byte[] bytes;
|
||||
try (InputStream input = jar.getInputStream(entry)) {
|
||||
bytes = input.readAllBytes();
|
||||
}
|
||||
entries.put(entry.getName(), new EntryIdentity(
|
||||
HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)),
|
||||
entry.getSize(), entry.getCrc(), entry.getTime(), entry.getComment(),
|
||||
entry.getExtra() == null ? "" : HexFormat.of().formatHex(entry.getExtra())));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private record EntryIdentity(String sha256, long size, long crc, long time, String comment, String extra) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user