| /* | |
| * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.util.jar.Attributes; | |
| import java.util.jar.Attributes.Name; | |
| import java.util.jar.Manifest; | |
| import java.util.List; | |
| import java.util.ArrayList; | |
| import org.testng.annotations.Test; | |
| import static org.testng.Assert.*; | |
| /** | |
| * @test | |
| * @bug 8066619 8351567 | |
| * @run testng ValueUtf8Coding | |
| * @summary Tests encoding and decoding manifest header values to and from | |
| * UTF-8 with the complete Unicode character set. | |
| */ /* | |
| * see also "../tools/launcher/UnicodeTest.java" for manifest attributes | |
| * parsed during launch | |
| */ | |
| public class ValueUtf8Coding { | |
| /** | |
| * Maximum number of bytes of UTF-8 encoded characters in one header value. | |
| * <p> | |
| * There are too many different Unicode code points (more than one million) | |
| * to fit all into one manifest value. The specifications state: | |
| * <q>Implementations should support 65535-byte (not character) header | |
| * values, and 65535 headers per file. They might run out of memory, | |
| * but there should not be hard-coded limits below these values.</q> | |
| * | |
| * @see <a | |
| * href="{@docRoot}/../specs/jar/jar.html#Notes_on_Manifest_and_Signature_Files"> | |
| * Notes on Manifest and Signature Files</a> | |
| */ | |
| static final int SUPPORTED_VALUE_LENGTH = 65535; | |
| /** | |
| * Returns {@code true} if {@code codePoint} is known not to be a supported | |
| * character in manifest header values. Explicitly forbidden in manifest | |
| * header values are according to a statement from the specifications: | |
| * <q>otherchar: any UTF-8 character except NUL, CR and LF</q>. | |
| * {@code NUL} ({@code 0x0}), however, works just fine and might have been | |
| * used and might still be. | |
| * | |
| * @see <a href="{@docRoot}/../specs/jar/jar.html#Section-Specification"> | |
| * Jar File Specification</a> | |
| */ | |
| static boolean isUnsupportedManifestValueCharacter(int codePoint) { | |
| return codePoint == '\r' /* CR */ || codePoint == '\n' /* LF */; | |
| }; | |
| /** | |
| * Produces a list of strings with all Unicode characters except those | |
| * explicitly invalid in manifest header values. | |
| * Each string is filled with as many characters as fit into | |
| * {@link #SUPPORTED_VALUE_LENGTH} bytes with UTF-8 encoding except the | |
| * last string which contains the remaining characters. Each of those | |
| * strings becomes a header value the number of which 65535 should be | |
| * supported per file. | |
| * | |
| * @see <a | |
| * href="{@docRoot}/../specs/jar/jar.html#Notes_on_Manifest_and_Signature_Files"> | |
| * Notes on Manifest and Signature Files</a> | |
| */ | |
| static List<String> produceValuesWithAllUnicodeCharacters() { | |
| ArrayList<String> values = new ArrayList<>(); | |
| byte[] valueBuf = new byte[SUPPORTED_VALUE_LENGTH]; | |
| int pos = 0; | |
| for (int codePoint = Character.MIN_CODE_POINT; | |
| codePoint <= Character.MAX_CODE_POINT; codePoint++) { | |
| if (isUnsupportedManifestValueCharacter(codePoint)) { | |
| continue; | |
| } | |
| byte[] charBuf = Character.toString(codePoint).getBytes(UTF_8); | |
| if (pos + charBuf.length > valueBuf.length) { | |
| values.add(new String(valueBuf, 0, pos, UTF_8)); | |
| pos = 0; | |
| } | |
| System.arraycopy(charBuf, 0, valueBuf, pos, charBuf.length); | |
| pos += charBuf.length; | |
| } | |
| if (pos > 0) { | |
| values.add(new String(valueBuf, 0, pos, UTF_8)); | |
| } | |
| // minimum number of headers supported is the same as the minimum size | |
| // of each header value in bytes | |
| assertTrue(values.size() <= SUPPORTED_VALUE_LENGTH); | |
| return values; | |
| } | |
| /** | |
| * Returns simple, valid, short, and distinct manifest header names. | |
| * The returned name cannot collide with "{@code Manifest-Version}" because | |
| * the returned string does not contain "{@code -}". | |
| */ | |
| static Name azName(int seed) { | |
| StringBuffer name = new StringBuffer(); | |
| do { | |
| name.insert(0, (char) (seed % 26 + (seed < 26 ? 'A' : 'a'))); | |
| seed = seed / 26 - 1; | |
| } while (seed >= 0); | |
| return new Name(name.toString()); | |
| } | |
| /** | |
| * Writes and reads a manifest with the complete Unicode character set. | |
| * The characters are grouped into manifest header values with about as | |
| * many bytes as allowed each, utilizing a single big manifest. | |
| * <p> | |
| * This test assumes that a manifest is encoded and decoded correctly if | |
| * writing and then reading it again results in a manifest with identical | |
| * values as the original. The test is not about other aspects of writing | |
| * and reading manifests than only that, given the fact and the way it | |
| * works for some characters such as the most widely and often used ones, | |
| * it also works for the complete Unicode character set just the same. | |
| * <p> | |
| * Only header values are tested. The set of allowed characters for header | |
| * names are much more limited and are a different topic entirely and most | |
| * simple ones are used here as necessary just to get valid and different | |
| * ones (see {@link #azName}). | |
| * <p> | |
| * Because the current implementation under test uses different portions | |
| * of code depending on where the value occurs to read or write, each | |
| * character is tested in each of the three positions:<ul> | |
| * <li>main attribute header,</li> | |
| * <li>named section name, and</li> | |
| * <li>named sections header values</li> | |
| * </ul> | |
| * Implementation of writing the main section headers in | |
| * {@link Attributes#writeMain(java.io.DataOutputStream)} differs from the | |
| * one writing named section headers in | |
| * {@link Attributes#write(java.io.DataOutputStream)} regarding the special | |
| * order of {@link Name#MANIFEST_VERSION} and | |
| * {@link Name#SIGNATURE_VERSION} and also | |
| * {@link Manifest#read(java.io.InputStream)} at least potentially reads | |
| * main sections differently than reading named sections names headers in | |
| * {@link Attributes#read(Manifest.FastInputStream, byte[])}. | |
| */ | |
| @Test | |
| public void testCompleteUnicodeCharacterSet() throws IOException { | |
| Manifest mf = new Manifest(); | |
| mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0"); | |
| List<String> values = produceValuesWithAllUnicodeCharacters(); | |
| for (int i = 0; i < values.size(); i++) { | |
| Name name = azName(i); | |
| String value = values.get(i); | |
| mf.getMainAttributes().put(name, value); | |
| Attributes attributes = new Attributes(); | |
| mf.getEntries().put(value, attributes); | |
| attributes.put(name, value); | |
| } | |
| mf = writeAndRead(mf); | |
| for (int i = 0; i < values.size(); i++) { | |
| String value = values.get(i); | |
| Name name = azName(i); | |
| assertEquals(mf.getMainAttributes().getValue(name), value, | |
| "main attributes header value"); | |
| Attributes attributes = mf.getAttributes(value); | |
| assertNotNull(attributes, "named section"); | |
| assertEquals(attributes.getValue(name), value, | |
| "named section attributes value"); | |
| } | |
| } | |
| static Manifest writeAndRead(Manifest mf) throws IOException { | |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
| mf.write(out); | |
| byte[] mfBytes = out.toByteArray(); | |
| ByteArrayInputStream in = new ByteArrayInputStream(mfBytes); | |
| return new Manifest(in); | |
| } | |
| } |