From fbdf282cf1521c29df050dbe916db5ebf34bd7e4 Mon Sep 17 00:00:00 2001 From: Christoph Rueger Date: Thu, 10 Oct 2024 22:24:02 +0200 Subject: [PATCH 1/4] 'nice' Manifest Writer This writes the manifest file with indention which is easier to read, while still considering the 72 chars limit per line. so technically still a valid manifest. borrowed from https://github.com/apache/felix-dev/blob/master/tools/maven-bundle-plugin/src/main/java/org/apache/felix/bundleplugin/ManifestWriter.java#L108 and https://github.com/apache/felix-dev/blob/master/utils/src/main/java/org/apache/felix/utils/manifest/Parser.java#L43 Signed-off-by: Christoph Rueger --- .../src/aQute/lib/manifest/ManifestUtil.java | 88 ++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java index 46b15e9cc0..947a4a9020 100644 --- a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java +++ b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java @@ -4,8 +4,10 @@ import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -40,6 +42,9 @@ public final class ManifestUtil { private static final byte[] EOL = new byte[] { '\r', '\n' }; + private static final byte[] EOL_INDENT = new byte[] { + '\r', '\n', ' ' + }; private static final byte[] SEPARATOR = new byte[] { ':', ' ' }; @@ -86,8 +91,28 @@ public static void write(Manifest manifest, OutputStream out) throws IOException private static void writeEntry(OutputStream out, Name name, String value) throws IOException { int width = write(out, 0, name.toString()); width = write(out, width, SEPARATOR); - write(out, width, value); - out.write(EOL); + + String[] parts = parseDelimitedString(value, ","); + if (parts.length > 1) { + write(out, 0, EOL_INDENT); + width = 1; + } + + for (int i = 0; i < parts.length; i++) { + if (i < parts.length - 1) { + width = write(out, width, parts[i]); + write(out, width, ","); + write(out, 0, EOL_INDENT); + } else { + write(out, width, parts[i]); + write(out, 0, EOL); + } + width = 1; + } + + // width = write(out, width, SEPARATOR); + // write(out, width, value); + // out.write(EOL); } /** @@ -170,4 +195,63 @@ private static Stream> sortedAttributes(Attributes attribute private static Map coerce(Attributes attributes) { return (Map) attributes; } + + /** + * Parses delimited string and returns an array containing the tokens. This + * parser obeys quotes, so the delimiter character will be ignored if it is + * inside of a quote. This method assumes that the quote character is not + * included in the set of delimiter characters. + * + * @param value the delimited string to parse. + * @param delim the characters delimiting the tokens. + * @return an array of string tokens or null if there were no tokens. + **/ + private static String[] parseDelimitedString(String value, String delim) { + if (value == null) { + value = ""; + } + + List list = new ArrayList<>(); + + int CHAR = 1; + int DELIMITER = 2; + int STARTQUOTE = 4; + int ENDQUOTE = 8; + + StringBuilder sb = new StringBuilder(); + + int expecting = (CHAR | DELIMITER | STARTQUOTE); + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + + boolean isDelimiter = (delim.indexOf(c) >= 0); + boolean isQuote = (c == '"'); + + if (isDelimiter && ((expecting & DELIMITER) > 0)) { + list.add(sb.toString() + .trim()); + sb.delete(0, sb.length()); + expecting = (CHAR | DELIMITER | STARTQUOTE); + } else if (isQuote && ((expecting & STARTQUOTE) > 0)) { + sb.append(c); + expecting = CHAR | ENDQUOTE; + } else if (isQuote && ((expecting & ENDQUOTE) > 0)) { + sb.append(c); + expecting = (CHAR | STARTQUOTE | DELIMITER); + } else if ((expecting & CHAR) > 0) { + sb.append(c); + } else { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + } + + String s = sb.toString() + .trim(); + if (s.length() > 0) { + list.add(s); + } + + return list.toArray(new String[list.size()]); + } } From 0503ff76aefc63a909e6e47847acc16e374a727b Mon Sep 17 00:00:00 2001 From: Christoph Rueger Date: Fri, 11 Oct 2024 00:05:58 +0200 Subject: [PATCH 2/4] fix two failing tests CorruptManifest > testCorruptJar() FAILED org.opentest4j.AssertionFailedError: expected: <. . > but was: <. .> at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151) at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132) at app//org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197) at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182) at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177) at app//org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1142) at app//test.CorruptManifest.testCorruptJar(CorruptManifest.java:59) MultiReleaseTest > testBuild() FAILED org.opentest4j.AssertionFailedError: expected: "" but was: "Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=9))"" at java.base@17.0.11/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base@17.0.11/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) at java.base@17.0.11/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base@17.0.11/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) at app//test.MultiReleaseTest.testBuild(MultiReleaseTest.java:96) Signed-off-by: Christoph Rueger check null Signed-off-by: Christoph Rueger --- .../src/aQute/lib/manifest/ManifestUtil.java | 70 +++++++++++++------ 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java index 947a4a9020..4a7200b0b4 100644 --- a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java +++ b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java @@ -5,17 +5,22 @@ import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.Manifest; import java.util.stream.Stream; +import org.osgi.framework.Constants; + /** * Unfortunately we have to write our own manifest :-( because of a stupid bug * in the manifest code. It tries to handle UTF-8 but the way it does it it @@ -50,6 +55,21 @@ public final class ManifestUtil { }; private static final int MAX_LENGTH = 72 - EOL.length; + @SuppressWarnings("deprecation") + private static final Set NICE_HEADERS = new HashSet<>( + Arrays.asList( + Constants.IMPORT_PACKAGE, + Constants.DYNAMICIMPORT_PACKAGE, + Constants.IMPORT_SERVICE, + Constants.REQUIRE_CAPABILITY, + Constants.EXPORT_PACKAGE, + Constants.EXPORT_SERVICE, + Constants.PROVIDE_CAPABILITY, + Constants.REQUIRE_BUNDLE, + Constants.BUNDLE_CLASSPATH + ) +); + private ManifestUtil() {} public static void write(Manifest manifest, OutputStream out) throws IOException { @@ -89,30 +109,41 @@ public static void write(Manifest manifest, OutputStream out) throws IOException * Write out an entry, handling proper unicode and line length constraints */ private static void writeEntry(OutputStream out, Name name, String value) throws IOException { - int width = write(out, 0, name.toString()); - width = write(out, width, SEPARATOR); - String[] parts = parseDelimitedString(value, ","); - if (parts.length > 1) { - write(out, 0, EOL_INDENT); - width = 1; - } + if(NICE_HEADERS.contains(name.toString())) { + int width = write(out, 0, name.toString()); + width = write(out, width, SEPARATOR); - for (int i = 0; i < parts.length; i++) { - if (i < parts.length - 1) { - width = write(out, width, parts[i]); - write(out, width, ","); - write(out, 0, EOL_INDENT); - } else { - write(out, width, parts[i]); + if (value == null || value.isEmpty()) { + // could be a Multi-Release Jar write(out, 0, EOL); + return; } - width = 1; + + String[] parts = parseDelimitedString(value, ","); + if (parts.length > 1) { + write(out, 0, EOL_INDENT); + width = 1; + } + + for (int i = 0; i < parts.length; i++) { + if (i < parts.length - 1) { + width = write(out, width, parts[i] + ","); + write(out, 0, EOL_INDENT); + } else { + width = write(out, width, parts[i]); + write(out, 0, EOL); + } + width = 1; + } + } + else { + int width = write(out, 0, name.toString()); + width = write(out, width, SEPARATOR); + write(out, width, value); + write(out, 0, EOL); } - // width = write(out, width, SEPARATOR); - // write(out, width, value); - // out.write(EOL); } /** @@ -144,8 +175,7 @@ private static int write(OutputStream out, int width, String s) throws IOExcepti private static int write(OutputStream out, int width, byte[] bytes) throws IOException { for (int position = 0, limit = bytes.length, remaining; (remaining = limit - position) > 0;) { if (width >= MAX_LENGTH) { - out.write(EOL); - out.write(' '); + out.write(EOL_INDENT); width = 1; } int count = Math.min(MAX_LENGTH - width, remaining); From 5b68b38b389c9c48847cafd7451fdd60e99df7af Mon Sep 17 00:00:00 2001 From: Christoph Rueger Date: Fri, 11 Oct 2024 00:40:53 +0200 Subject: [PATCH 3/4] add Private-Package header Signed-off-by: Christoph Rueger --- aQute.libg/src/aQute/lib/manifest/ManifestUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java index 4a7200b0b4..4703459f6c 100644 --- a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java +++ b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java @@ -66,7 +66,7 @@ public final class ManifestUtil { Constants.EXPORT_SERVICE, Constants.PROVIDE_CAPABILITY, Constants.REQUIRE_BUNDLE, - Constants.BUNDLE_CLASSPATH + Constants.BUNDLE_CLASSPATH, "Private-Package" ) ); From 9bba2880b556418c156328fad121aef698c05848 Mon Sep 17 00:00:00 2001 From: Christoph Rueger Date: Fri, 11 Oct 2024 07:12:32 +0200 Subject: [PATCH 4/4] add nice parameter and default=true method that way 'nice' could be disabled by callers. Signed-off-by: Christoph Rueger --- .../src/aQute/lib/manifest/ManifestUtil.java | 29 +++++++++++++------ .../src/aQute/lib/manifest/package-info.java | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java index 4703459f6c..467487dad0 100644 --- a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java +++ b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java @@ -72,7 +72,16 @@ public final class ManifestUtil { private ManifestUtil() {} + /** + * Writes out the manifest, nicely formatted. Use + * {@link #write(Manifest, OutputStream, boolean)} to control 'nice' + * formatting. + */ public static void write(Manifest manifest, OutputStream out) throws IOException { + write(manifest, out, true); + } + + public static void write(Manifest manifest, OutputStream out, boolean nice) throws IOException { Attributes mainAttributes = manifest.getMainAttributes(); Stream> sortedAttributes = sortedAttributes(mainAttributes); @@ -83,12 +92,12 @@ public static void write(Manifest manifest, OutputStream out) throws IOException versionValue = mainAttributes.getValue(versionName); } if (versionValue != null) { - writeEntry(out, versionName, versionValue); + writeEntry(out, versionName, versionValue, nice); Name filterName = versionName; // Name.equals is case-insensitive sortedAttributes = sortedAttributes.filter(e -> !Objects.equals(e.getKey(), filterName)); } - writeAttributes(out, sortedAttributes); + writeAttributes(out, sortedAttributes, nice); out.write(EOL); for (Iterator> iterator = manifest.getEntries() @@ -97,8 +106,8 @@ public static void write(Manifest manifest, OutputStream out) throws IOException .sorted(Entry.comparingByKey()) .iterator(); iterator.hasNext();) { Entry entry = iterator.next(); - writeEntry(out, NAME, entry.getKey()); - writeAttributes(out, sortedAttributes(entry.getValue())); + writeEntry(out, NAME, entry.getKey(), nice); + writeAttributes(out, sortedAttributes(entry.getValue()), nice); out.write(EOL); } @@ -108,9 +117,9 @@ public static void write(Manifest manifest, OutputStream out) throws IOException /** * Write out an entry, handling proper unicode and line length constraints */ - private static void writeEntry(OutputStream out, Name name, String value) throws IOException { + private static void writeEntry(OutputStream out, Name name, String value, boolean nice) throws IOException { - if(NICE_HEADERS.contains(name.toString())) { + if (nice && NICE_HEADERS.contains(name.toString())) { int width = write(out, 0, name.toString()); width = write(out, width, SEPARATOR); @@ -189,14 +198,16 @@ private static int write(OutputStream out, int width, byte[] bytes) throws IOExc /** * Output an Attributes map. We sort the map keys. * - * @param value the attributes * @param out the output stream + * @param attributes the attributes + * @param nice nice formatting or not * @throws IOException when something fails */ - private static void writeAttributes(OutputStream out, Stream> attributes) throws IOException { + private static void writeAttributes(OutputStream out, Stream> attributes, boolean nice) + throws IOException { for (Iterator> iterator = attributes.iterator(); iterator.hasNext();) { Entry attribute = iterator.next(); - writeEntry(out, attribute.getKey(), attribute.getValue()); + writeEntry(out, attribute.getKey(), attribute.getValue(), nice); } } diff --git a/aQute.libg/src/aQute/lib/manifest/package-info.java b/aQute.libg/src/aQute/lib/manifest/package-info.java index 7912793ebb..ef56e5d036 100644 --- a/aQute.libg/src/aQute/lib/manifest/package-info.java +++ b/aQute.libg/src/aQute/lib/manifest/package-info.java @@ -1,4 +1,4 @@ -@Version("1.0.0") +@Version("1.1.0") package aQute.lib.manifest; import org.osgi.annotation.versioning.Version;