/* * Copyright (c) 1997, 2018, 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * 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. */ package java.util.jar; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.Security; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Set; import sun.security.util.SecurityProperties; import sun.util.logging.PlatformLogger; /** * The Attributes class maps Manifest attribute names to associated string * values. Valid attribute names are case-insensitive, are restricted to * the ASCII characters in the set [0-9a-zA-Z_-], and cannot exceed 70 * characters in length. There must be a colon and a SPACE after the name; * the combined length will not exceed 72 characters. * Attribute values can contain any characters and * will be UTF8-encoded when written to the output stream. See the * JAR File Specification * for more information about valid attribute names and values. * *

This map and its views have a predictable iteration order, namely the * order that keys were inserted into the map, as with {@link LinkedHashMap}. * * @author David Connelly * @see Manifest * @since 1.2 */ public class Attributes implements Map, Cloneable { /** * The attribute name-value mappings. */ protected Map map; private static final boolean jarPathInExceptionText = SecurityProperties.includedInExceptions("jarPath"); /** * Constructs a new, empty Attributes object with default size. */ public Attributes() { this(11); } /** * Constructs a new, empty Attributes object with the specified * initial size. * * @param size the initial number of attributes */ public Attributes(int size) { map = new LinkedHashMap<>(size); } /** * Constructs a new Attributes object with the same attribute name-value * mappings as in the specified Attributes. * * @param attr the specified Attributes */ public Attributes(Attributes attr) { map = new LinkedHashMap<>(attr); } /** * Returns the value of the specified attribute name, or null if the * attribute name was not found. * * @param name the attribute name * @return the value of the specified attribute name, or null if * not found. */ public Object get(Object name) { return map.get(name); } /** * Returns the value of the specified attribute name, specified as * a string, or null if the attribute was not found. The attribute * name is case-insensitive. *

* This method is defined as: *

     *      return (String)get(new Attributes.Name((String)name));
     * 
* * @param name the attribute name as a string * @return the String value of the specified attribute name, or null if * not found. * @throws IllegalArgumentException if the attribute name is invalid */ public String getValue(String name) { return (String)get(Name.of(name)); } /** * Returns the value of the specified Attributes.Name, or null if the * attribute was not found. *

* This method is defined as: *

     *     return (String)get(name);
     * 
* * @param name the Attributes.Name object * @return the String value of the specified Attribute.Name, or null if * not found. */ public String getValue(Name name) { return (String)get(name); } /** * Associates the specified value with the specified attribute name * (key) in this Map. If the Map previously contained a mapping for * the attribute name, the old value is replaced. * * @param name the attribute name * @param value the attribute value * @return the previous value of the attribute, or null if none * @exception ClassCastException if the name is not a Attributes.Name * or the value is not a String */ public Object put(Object name, Object value) { return map.put((Attributes.Name)name, (String)value); } /** * Associates the specified value with the specified attribute name, * specified as a String. The attributes name is case-insensitive. * If the Map previously contained a mapping for the attribute name, * the old value is replaced. *

* This method is defined as: *

     *      return (String)put(new Attributes.Name(name), value);
     * 
* * @param name the attribute name as a string * @param value the attribute value * @return the previous value of the attribute, or null if none * @exception IllegalArgumentException if the attribute name is invalid */ public String putValue(String name, String value) { return (String)put(Name.of(name), value); } /** * Removes the attribute with the specified name (key) from this Map. * Returns the previous attribute value, or null if none. * * @param name attribute name * @return the previous value of the attribute, or null if none */ public Object remove(Object name) { return map.remove(name); } /** * Returns true if this Map maps one or more attribute names (keys) * to the specified value. * * @param value the attribute value * @return true if this Map maps one or more attribute names to * the specified value */ public boolean containsValue(Object value) { return map.containsValue(value); } /** * Returns true if this Map contains the specified attribute name (key). * * @param name the attribute name * @return true if this Map contains the specified attribute name */ public boolean containsKey(Object name) { return map.containsKey(name); } /** * Copies all of the attribute name-value mappings from the specified * Attributes to this Map. Duplicate mappings will be replaced. * * @param attr the Attributes to be stored in this map * @exception ClassCastException if attr is not an Attributes */ public void putAll(Map attr) { // ## javac bug? if (!Attributes.class.isInstance(attr)) throw new ClassCastException(); for (Map.Entry me : (attr).entrySet()) put(me.getKey(), me.getValue()); } /** * Removes all attributes from this Map. */ public void clear() { map.clear(); } /** * Returns the number of attributes in this Map. */ public int size() { return map.size(); } /** * Returns true if this Map contains no attributes. */ public boolean isEmpty() { return map.isEmpty(); } /** * Returns a Set view of the attribute names (keys) contained in this Map. */ public Set keySet() { return map.keySet(); } /** * Returns a Collection view of the attribute values contained in this Map. */ public Collection values() { return map.values(); } /** * Returns a Collection view of the attribute name-value mappings * contained in this Map. */ public Set> entrySet() { return map.entrySet(); } /** * Compares the specified Attributes object with this Map for equality. * Returns true if the given object is also an instance of Attributes * and the two Attributes objects represent the same mappings. * * @param o the Object to be compared * @return true if the specified Object is equal to this Map */ public boolean equals(Object o) { return map.equals(o); } /** * Returns the hash code value for this Map. */ public int hashCode() { return map.hashCode(); } /** * Returns a copy of the Attributes, implemented as follows: *
     *     public Object clone() { return new Attributes(this); }
     * 
* Since the attribute names and values are themselves immutable, * the Attributes returned can be safely modified without affecting * the original. */ public Object clone() { return new Attributes(this); } /* * Writes the current attributes to the specified data output stream. * XXX Need to handle UTF8 values and break up lines longer than 72 bytes */ @SuppressWarnings("deprecation") void write(DataOutputStream os) throws IOException { for (Entry e : entrySet()) { StringBuffer buffer = new StringBuffer( ((Name) e.getKey()).toString()); buffer.append(": "); String value = (String) e.getValue(); if (value != null) { byte[] vb = value.getBytes("UTF8"); value = new String(vb, 0, 0, vb.length); } buffer.append(value); Manifest.make72Safe(buffer); buffer.append("\r\n"); os.writeBytes(buffer.toString()); } os.writeBytes("\r\n"); } /* * Writes the current attributes to the specified data output stream, * make sure to write out the MANIFEST_VERSION or SIGNATURE_VERSION * attributes first. * * XXX Need to handle UTF8 values and break up lines longer than 72 bytes */ @SuppressWarnings("deprecation") void writeMain(DataOutputStream out) throws IOException { // write out the *-Version header first, if it exists String vername = Name.MANIFEST_VERSION.toString(); String version = getValue(vername); if (version == null) { vername = Name.SIGNATURE_VERSION.toString(); version = getValue(vername); } if (version != null) { out.writeBytes(vername+": "+version+"\r\n"); } // write out all attributes except for the version // we wrote out earlier for (Entry e : entrySet()) { String name = ((Name) e.getKey()).toString(); if ((version != null) && !(name.equalsIgnoreCase(vername))) { StringBuffer buffer = new StringBuffer(name); buffer.append(": "); String value = (String) e.getValue(); if (value != null) { byte[] vb = value.getBytes("UTF8"); value = new String(vb, 0, 0, vb.length); } buffer.append(value); Manifest.make72Safe(buffer); buffer.append("\r\n"); out.writeBytes(buffer.toString()); } } out.writeBytes("\r\n"); } /* * Reads attributes from the specified input stream. * XXX Need to handle UTF8 values. */ void read(Manifest.FastInputStream is, byte[] lbuf) throws IOException { read(is, lbuf, null, 0); } @SuppressWarnings("deprecation") int read(Manifest.FastInputStream is, byte[] lbuf, String filename, int offset) throws IOException { String name = null, value; byte[] lastline = null; int lineNumber = offset; int len; while ((len = is.readLine(lbuf)) != -1) { boolean lineContinued = false; byte c = lbuf[--len]; lineNumber++; if (c != '\n' && c != '\r') { throw new IOException("line too long (" + getErrorPosition(filename, lineNumber) + ")"); } if (len > 0 && lbuf[len-1] == '\r') { --len; } if (len == 0) { break; } int i = 0; if (lbuf[0] == ' ') { // continuation of previous line if (name == null) { throw new IOException("misplaced continuation line (" + getErrorPosition(filename, lineNumber) + ")"); } lineContinued = true; byte[] buf = new byte[lastline.length + len - 1]; System.arraycopy(lastline, 0, buf, 0, lastline.length); System.arraycopy(lbuf, 1, buf, lastline.length, len - 1); if (is.peek() == ' ') { lastline = buf; continue; } value = new String(buf, 0, buf.length, "UTF8"); lastline = null; } else { while (lbuf[i++] != ':') { if (i >= len) { throw new IOException("invalid header field (" + getErrorPosition(filename, lineNumber) + ")"); } } if (lbuf[i++] != ' ') { throw new IOException("invalid header field (" + getErrorPosition(filename, lineNumber) + ")"); } name = new String(lbuf, 0, 0, i - 2); if (is.peek() == ' ') { lastline = new byte[len - i]; System.arraycopy(lbuf, i, lastline, 0, len - i); continue; } value = new String(lbuf, i, len - i, "UTF8"); } try { if ((putValue(name, value) != null) && (!lineContinued)) { PlatformLogger.getLogger("java.util.jar").warning( "Duplicate name in Manifest: " + name + ".\n" + "Ensure that the manifest does not " + "have duplicate entries, and\n" + "that blank lines separate " + "individual sections in both your\n" + "manifest and in the META-INF/MANIFEST.MF " + "entry in the jar file."); } } catch (IllegalArgumentException e) { throw new IOException("invalid header field name: " + name + " (" + getErrorPosition(filename, lineNumber) + ")"); } } return lineNumber; } static String getErrorPosition(String filename, final int lineNumber) { if (filename == null || !jarPathInExceptionText) { return "line " + lineNumber; } final File file = new File(filename); return AccessController.doPrivileged(new PrivilegedAction() { public String run() { return file.getAbsolutePath() + ":" + lineNumber; } }); } /** * The Attributes.Name class represents an attribute name stored in * this Map. Valid attribute names are case-insensitive, are restricted * to the ASCII characters in the set [0-9a-zA-Z_-], and cannot exceed * 70 characters in length. Attribute values can contain any characters * and will be UTF8-encoded when written to the output stream. See the * JAR File Specification * for more information about valid attribute names and values. */ public static class Name { private final String name; private final int hashCode; /** * Avoid allocation for common Names */ private static final Map KNOWN_NAMES; static final Name of(String name) { Name n = KNOWN_NAMES.get(name); if (n != null) { return n; } return new Name(name); } /** * Constructs a new attribute name using the given string name. * * @param name the attribute string name * @exception IllegalArgumentException if the attribute name was * invalid * @exception NullPointerException if the attribute name was null */ public Name(String name) { this.hashCode = hash(name); this.name = name.intern(); } // Checks the string is valid private final int hash(String name) { Objects.requireNonNull(name, "name"); int len = name.length(); if (len > 70 || len == 0) { throw new IllegalArgumentException(name); } // Calculate hash code case insensitively int h = 0; for (int i = 0; i < len; i++) { char c = name.charAt(i); if (c >= 'a' && c <= 'z') { // hashcode must be identical for upper and lower case h = h * 31 + (c - 0x20); } else if ((c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-')) { h = h * 31 + c; } else { throw new IllegalArgumentException(name); } } return h; } /** * Compares this attribute name to another for equality. * @param o the object to compare * @return true if this attribute name is equal to the * specified attribute object */ public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof Name) { Name other = (Name)o; return other.name.equalsIgnoreCase(name); } else { return false; } } /** * Computes the hash value for this attribute name. */ public int hashCode() { return hashCode; } /** * Returns the attribute name as a String. */ public String toString() { return name; } /** * {@code Name} object for {@code Manifest-Version} * manifest attribute. This attribute indicates the version number * of the manifest standard to which a JAR file's manifest conforms. * @see * Manifest and Signature Specification */ public static final Name MANIFEST_VERSION = new Name("Manifest-Version"); /** * {@code Name} object for {@code Signature-Version} * manifest attribute used when signing JAR files. * @see * Manifest and Signature Specification */ public static final Name SIGNATURE_VERSION = new Name("Signature-Version"); /** * {@code Name} object for {@code Content-Type} * manifest attribute. */ public static final Name CONTENT_TYPE = new Name("Content-Type"); /** * {@code Name} object for {@code Class-Path} * manifest attribute. * @see * JAR file specification */ public static final Name CLASS_PATH = new Name("Class-Path"); /** * {@code Name} object for {@code Main-Class} manifest * attribute used for launching applications packaged in JAR files. * The {@code Main-Class} attribute is used in conjunction * with the {@code -jar} command-line option of the * {@code java} application launcher. */ public static final Name MAIN_CLASS = new Name("Main-Class"); /** * {@code Name} object for {@code Sealed} manifest attribute * used for sealing. * @see * Package Sealing */ public static final Name SEALED = new Name("Sealed"); /** * {@code Name} object for {@code Extension-List} manifest attribute * used for the extension mechanism that is no longer supported. */ public static final Name EXTENSION_LIST = new Name("Extension-List"); /** * {@code Name} object for {@code Extension-Name} manifest attribute. * used for the extension mechanism that is no longer supported. */ public static final Name EXTENSION_NAME = new Name("Extension-Name"); /** * {@code Name} object for {@code Extension-Installation} manifest attribute. * * @deprecated Extension mechanism is no longer supported. */ @Deprecated public static final Name EXTENSION_INSTALLATION = new Name("Extension-Installation"); /** * {@code Name} object for {@code Implementation-Title} * manifest attribute used for package versioning. */ public static final Name IMPLEMENTATION_TITLE = new Name("Implementation-Title"); /** * {@code Name} object for {@code Implementation-Version} * manifest attribute used for package versioning. */ public static final Name IMPLEMENTATION_VERSION = new Name("Implementation-Version"); /** * {@code Name} object for {@code Implementation-Vendor} * manifest attribute used for package versioning. */ public static final Name IMPLEMENTATION_VENDOR = new Name("Implementation-Vendor"); /** * {@code Name} object for {@code Implementation-Vendor-Id} * manifest attribute. * * @deprecated Extension mechanism is no longer supported. */ @Deprecated public static final Name IMPLEMENTATION_VENDOR_ID = new Name("Implementation-Vendor-Id"); /** * {@code Name} object for {@code Implementation-URL} * manifest attribute. * * @deprecated Extension mechanism is no longer supported. */ @Deprecated public static final Name IMPLEMENTATION_URL = new Name("Implementation-URL"); /** * {@code Name} object for {@code Specification-Title} * manifest attribute used for package versioning. */ public static final Name SPECIFICATION_TITLE = new Name("Specification-Title"); /** * {@code Name} object for {@code Specification-Version} * manifest attribute used for package versioning. */ public static final Name SPECIFICATION_VERSION = new Name("Specification-Version"); /** * {@code Name} object for {@code Specification-Vendor} * manifest attribute used for package versioning. */ public static final Name SPECIFICATION_VENDOR = new Name("Specification-Vendor"); /** * {@code Name} object for {@code Multi-Release} * manifest attribute that indicates this is a multi-release JAR file. * * @since 9 */ public static final Name MULTI_RELEASE = new Name("Multi-Release"); private static void addName(Map names, Name name) { names.put(name.name, name); } static { var names = new HashMap(64); addName(names, MANIFEST_VERSION); addName(names, SIGNATURE_VERSION); addName(names, CONTENT_TYPE); addName(names, CLASS_PATH); addName(names, MAIN_CLASS); addName(names, SEALED); addName(names, EXTENSION_LIST); addName(names, EXTENSION_NAME); addName(names, IMPLEMENTATION_TITLE); addName(names, IMPLEMENTATION_VERSION); addName(names, IMPLEMENTATION_VENDOR); addName(names, SPECIFICATION_TITLE); addName(names, SPECIFICATION_VERSION); addName(names, SPECIFICATION_VENDOR); addName(names, MULTI_RELEASE); // Common attributes used in MANIFEST.MF et.al; adding these has a // small footprint cost, but is likely to be quickly paid for by // reducing allocation when reading and parsing typical manifests addName(names, new Name("Add-Exports")); addName(names, new Name("Add-Opens")); addName(names, new Name("Ant-Version")); addName(names, new Name("Archiver-Version")); addName(names, new Name("Build-Jdk")); addName(names, new Name("Built-By")); addName(names, new Name("Bnd-LastModified")); addName(names, new Name("Bundle-Description")); addName(names, new Name("Bundle-DocURL")); addName(names, new Name("Bundle-License")); addName(names, new Name("Bundle-ManifestVersion")); addName(names, new Name("Bundle-Name")); addName(names, new Name("Bundle-Vendor")); addName(names, new Name("Bundle-Version")); addName(names, new Name("Bundle-SymbolicName")); addName(names, new Name("Created-By")); addName(names, new Name("Export-Package")); addName(names, new Name("Import-Package")); addName(names, new Name("Name")); addName(names, new Name("SHA1-Digest")); addName(names, new Name("X-Compile-Source-JDK")); addName(names, new Name("X-Compile-Target-JDK")); KNOWN_NAMES = names; } } }