diff --git a/jarjar/src/main/java/com/eed3si9n/jarjar/ResourceProcessor.java b/jarjar/src/main/java/com/eed3si9n/jarjar/ResourceProcessor.java index 536cd857..78fde49e 100644 --- a/jarjar/src/main/java/com/eed3si9n/jarjar/ResourceProcessor.java +++ b/jarjar/src/main/java/com/eed3si9n/jarjar/ResourceProcessor.java @@ -16,12 +16,17 @@ package com.eed3si9n.jarjar; -import com.eed3si9n.jarjar.util.*; +import com.eed3si9n.jarjar.util.EntryStruct; +import com.eed3si9n.jarjar.util.JarProcessor; + import java.io.IOException; -import java.util.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; class ResourceProcessor implements JarProcessor { + private final static String META_INF_SERVICES = "META-INF/services/"; private PackageRemapper pr; public ResourceProcessor(PackageRemapper pr) { @@ -29,9 +34,49 @@ public ResourceProcessor(PackageRemapper pr) { } public boolean process(EntryStruct struct) throws IOException { - if (!struct.name.endsWith(".class")) - struct.name = pr.mapPath(struct.name); + switch (identify(struct)) { + case CLASS_FILE: + break; + case SERVICE_PROVIDER_CONFIGURATION: + struct.name = remapService(struct.name); + struct.data = remapServiceProviders(struct.data); + break; + case OTHER: + struct.name = pr.mapPath(struct.name); + break; + } return true; } + + private String remapService(String serviceFile) { + int idx = serviceFile.lastIndexOf('/'); + return META_INF_SERVICES + pr.mapValue(serviceFile.substring(idx + 1)); + } + + private byte[] remapServiceProviders(byte[] providers) { + // Provider configuration is encoded in UTF-8 + // The file can also have comments and whitespaces + String s = new String(providers, StandardCharsets.UTF_8); + + String mapped = Arrays.stream(s.split(System.lineSeparator())) + .map(l -> (String) pr.mapValue(Arrays.stream(l.split("#")).findFirst().orElse("").trim())) + .collect(Collectors.joining(System.lineSeparator())); + return mapped.getBytes(StandardCharsets.UTF_8); + } + + private Resource identify(EntryStruct struct) { + if (struct.name.endsWith(".class")) + return Resource.CLASS_FILE; + // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ServiceLoader.html + if (struct.name.startsWith(META_INF_SERVICES) && !struct.name.equals(META_INF_SERVICES)) + return Resource.SERVICE_PROVIDER_CONFIGURATION; + return Resource.OTHER; + } + + private enum Resource { + CLASS_FILE, + SERVICE_PROVIDER_CONFIGURATION, + OTHER + } } diff --git a/jarjar/src/main/java/com/eed3si9n/jarjar/RulesFileParser.java b/jarjar/src/main/java/com/eed3si9n/jarjar/RulesFileParser.java index 5a4273d7..5792836a 100644 --- a/jarjar/src/main/java/com/eed3si9n/jarjar/RulesFileParser.java +++ b/jarjar/src/main/java/com/eed3si9n/jarjar/RulesFileParser.java @@ -34,7 +34,7 @@ public static List parse(String value) throws IOException { private static String stripComment(String in) { int p = in.indexOf("#"); - return p < 0 ? in : in.substring(0, p); + return p < 0 ? in.trim() : in.substring(0, p).trim(); } private static List parse(Reader r) throws IOException { diff --git a/jarjar/src/test/java/com/eed3si9n/jarjar/ResourceProcessorTest.java b/jarjar/src/test/java/com/eed3si9n/jarjar/ResourceProcessorTest.java new file mode 100644 index 00000000..11a84321 --- /dev/null +++ b/jarjar/src/test/java/com/eed3si9n/jarjar/ResourceProcessorTest.java @@ -0,0 +1,74 @@ +package com.eed3si9n.jarjar; + +import com.eed3si9n.jarjar.util.EntryStruct; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.*; + +public class ResourceProcessorTest { + + private ResourceProcessor processor; + + @Before + public void setUp() throws Exception { + String rules = "rule org.example.** something.shaded.@0"; + List parsed = (List)(List) RulesFileParser.parse(rules); + processor = new ResourceProcessor(new PackageRemapper(parsed, true)); + } + + @Test + public void testClassFile() throws IOException { + EntryStruct entryStruct = new EntryStruct(); + entryStruct.name = "org/example/Object.class"; + entryStruct.data = new byte[]{0x10}; + + assertTrue(processor.process(entryStruct)); + assertEquals(entryStruct.name, "org/example/Object.class"); + assertArrayEquals(entryStruct.data, new byte[]{0x10}); + } + + @Test + public void testServiceProviderConfig() throws IOException { + String original = "org.example.Impl # comment" + System.lineSeparator() + + "org.example.AnotherImpl" + System.lineSeparator() + + "#" + System.lineSeparator() + + System.lineSeparator() + + " org.another.Impl"; + String expected = "something.shaded.org.example.Impl" + System.lineSeparator() + + "something.shaded.org.example.AnotherImpl" + System.lineSeparator() + + System.lineSeparator() + + System.lineSeparator() + + "org.another.Impl"; + EntryStruct entryStruct = new EntryStruct(); + entryStruct.name = "META-INF/services/org.example.Service"; + entryStruct.data = original.getBytes(StandardCharsets.UTF_8); + + assertTrue(processor.process(entryStruct)); + assertEquals(entryStruct.name, "META-INF/services/something.shaded.org.example.Service"); + assertArrayEquals(entryStruct.data, expected.getBytes(StandardCharsets.UTF_8)); + } + + @Test + public void testOtherResource() throws IOException { + EntryStruct entryStruct = new EntryStruct(); + entryStruct.name = "org/example/file.txt"; + entryStruct.data = new byte[]{0x10}; + + assertTrue(processor.process(entryStruct)); + assertEquals(entryStruct.name, "something/shaded/org/example/file.txt"); + assertArrayEquals(entryStruct.data, new byte[]{0x10}); + + EntryStruct entryStruct2 = new EntryStruct(); + entryStruct2.name = "another_org/example/file.txt"; + entryStruct2.data = new byte[]{0x10}; + + assertTrue(processor.process(entryStruct2)); + assertEquals(entryStruct2.name, "another_org/example/file.txt"); + assertArrayEquals(entryStruct.data, new byte[]{0x10}); + } +} \ No newline at end of file