| /* JarUtils.java -- Utility methods for reading/writing Manifest[-like] files | |
| Copyright (C) 2006, 2007 Free Software Foundation, Inc. | |
| This file is part of GNU Classpath. | |
| GNU Classpath is free software; you can redistribute it and/or modify | |
| it under the terms of the GNU General Public License as published by | |
| the Free Software Foundation; either version 2, or (at your option) | |
| any later version. | |
| GNU Classpath 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 for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with GNU Classpath; see the file COPYING. If not, write to the | |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 USA. | |
| Linking this library statically or dynamically with other modules is | |
| making a combined work based on this library. Thus, the terms and | |
| conditions of the GNU General Public License cover the whole | |
| combination. | |
| As a special exception, the copyright holders of this library give you | |
| permission to link this library with independent modules to produce an | |
| executable, regardless of the license terms of these independent | |
| modules, and to copy and distribute the resulting executable under | |
| terms of your choice, provided that you also meet, for each linked | |
| independent module, the terms and conditions of the license of that | |
| module. An independent module is a module which is not derived from | |
| or based on this library. If you modify this library, you may extend | |
| this exception to your version of the library, but you are not | |
| obligated to do so. If you do not wish to do so, delete this | |
| exception statement from your version. */ | |
| package gnu.java.util.jar; | |
| import gnu.classpath.SystemProperties; | |
| import java.io.BufferedOutputStream; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStream; | |
| import java.util.Iterator; | |
| import java.util.Map; | |
| import java.util.jar.Attributes; | |
| import java.util.jar.JarException; | |
| import java.util.jar.Attributes.Name; | |
| import java.util.logging.Logger; | |
| /** | |
| * Utility methods for reading and writing JAR <i>Manifest</i> and | |
| * <i>Manifest-like</i> files. | |
| * <p> | |
| * JAR-related files that resemble <i>Manifest</i> files are Signature files | |
| * (with an <code>.SF</code> extension) found in signed JARs. | |
| */ | |
| public abstract class JarUtils | |
| { | |
| // We used to log here, but this causes problems during bootstrap, | |
| // and it didn't seem worthwhile to preserve this. Still, this | |
| // might be useful for debugging. | |
| // private static final Logger log = Logger.getLogger(JarUtils.class.getName()); | |
| public static final String META_INF = "META-INF/"; | |
| public static final String DSA_SUFFIX = ".DSA"; | |
| public static final String SF_SUFFIX = ".SF"; | |
| public static final String NAME = "Name"; | |
| /** | |
| * The original string representation of the manifest version attribute name. | |
| */ | |
| public static final String MANIFEST_VERSION = "Manifest-Version"; | |
| /** | |
| * The original string representation of the signature version attribute | |
| * name. | |
| */ | |
| public static final String SIGNATURE_VERSION = "Signature-Version"; | |
| /** Platform-independent line-ending. */ | |
| public static final byte[] CRLF = new byte[] { 0x0D, 0x0A }; | |
| private static final String DEFAULT_MF_VERSION = "1.0"; | |
| private static final String DEFAULT_SF_VERSION = "1.0"; | |
| private static final Name CREATED_BY = new Name("Created-By"); | |
| private static final String CREATOR = SystemProperties.getProperty("java.version") | |
| + " (" | |
| + SystemProperties.getProperty("java.vendor") | |
| + ")"; | |
| // default 0-arguments constructor | |
| // Methods for reading Manifest files from InputStream ---------------------- | |
| public static void | |
| readMFManifest(Attributes attr, Map entries, InputStream in) | |
| throws IOException | |
| { | |
| BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8")); | |
| readMainSection(attr, br); | |
| readIndividualSections(entries, br); | |
| } | |
| public static void | |
| readSFManifest(Attributes attr, Map entries, InputStream in) | |
| throws IOException | |
| { | |
| BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8")); | |
| String version_header = Name.SIGNATURE_VERSION.toString(); | |
| try | |
| { | |
| String version = expectHeader(version_header, br); | |
| attr.putValue(SIGNATURE_VERSION, version); | |
| // This may cause problems during VM bootstrap. | |
| // if (! DEFAULT_SF_VERSION.equals(version)) | |
| // log.warning("Unexpected version number: " + version | |
| // + ". Continue (but may fail later)"); | |
| } | |
| catch (IOException ioe) | |
| { | |
| throw new JarException("Signature file MUST start with a " | |
| + version_header + ": " + ioe.getMessage()); | |
| } | |
| read_attributes(attr, br); | |
| // read individual sections | |
| String s = br.readLine(); | |
| while (s != null && s.length() > 0) | |
| { | |
| Attributes eAttr = readSectionName(s, br, entries); | |
| read_attributes(eAttr, br); | |
| s = br.readLine(); | |
| } | |
| } | |
| private static void readMainSection(Attributes attr, BufferedReader br) | |
| throws IOException | |
| { | |
| // According to the spec we should actually call read_version_info() here. | |
| read_attributes(attr, br); | |
| // Explicitly set Manifest-Version attribute if not set in Main | |
| // attributes of Manifest. | |
| // XXX (rsn): why 0.0 and not 1.0? | |
| if (attr.getValue(Name.MANIFEST_VERSION) == null) | |
| attr.putValue(MANIFEST_VERSION, "0.0"); | |
| } | |
| private static void readIndividualSections(Map entries, BufferedReader br) | |
| throws IOException | |
| { | |
| String s = br.readLine(); | |
| while (s != null && (! s.equals(""))) | |
| { | |
| Attributes attr = readSectionName(s, br, entries); | |
| read_attributes(attr, br); | |
| s = br.readLine(); | |
| } | |
| } | |
| /** | |
| * Pedantic method that requires the next attribute in the Manifest to be the | |
| * "Manifest-Version". This follows the Manifest spec closely but reject some | |
| * jar Manifest files out in the wild. | |
| */ | |
| private static void readVersionInfo(Attributes attr, BufferedReader br) | |
| throws IOException | |
| { | |
| String version_header = Name.MANIFEST_VERSION.toString(); | |
| try | |
| { | |
| String value = expectHeader(version_header, br); | |
| attr.putValue(MANIFEST_VERSION, value); | |
| } | |
| catch (IOException ioe) | |
| { | |
| throw new JarException("Manifest should start with a " + version_header | |
| + ": " + ioe.getMessage()); | |
| } | |
| } | |
| private static String expectHeader(String header, BufferedReader br) | |
| throws IOException | |
| { | |
| String s = br.readLine(); | |
| if (s == null) | |
| throw new JarException("unexpected end of file"); | |
| return expectHeader(header, br, s); | |
| } | |
| private static void read_attributes(Attributes attr, BufferedReader br) | |
| throws IOException | |
| { | |
| String s = br.readLine(); | |
| while (s != null && (! s.equals(""))) | |
| { | |
| readAttribute(attr, s, br); | |
| s = br.readLine(); | |
| } | |
| } | |
| private static void | |
| readAttribute(Attributes attr, String s, BufferedReader br) throws IOException | |
| { | |
| try | |
| { | |
| int colon = s.indexOf(": "); | |
| String name = s.substring(0, colon); | |
| String value_start = s.substring(colon + 2); | |
| String value = readHeaderValue(value_start, br); | |
| attr.putValue(name, value); | |
| } | |
| catch (IndexOutOfBoundsException iobe) | |
| { | |
| throw new JarException("Manifest contains a bad header: " + s); | |
| } | |
| } | |
| private static String readHeaderValue(String s, BufferedReader br) | |
| throws IOException | |
| { | |
| boolean try_next = true; | |
| while (try_next) | |
| { | |
| // Lets see if there is something on the next line | |
| br.mark(1); | |
| if (br.read() == ' ') | |
| s += br.readLine(); | |
| else | |
| { | |
| br.reset(); | |
| try_next = false; | |
| } | |
| } | |
| return s; | |
| } | |
| private static Attributes | |
| readSectionName(String s, BufferedReader br, Map entries) throws JarException | |
| { | |
| try | |
| { | |
| String name = expectHeader(NAME, br, s); | |
| Attributes attr = new Attributes(); | |
| entries.put(name, attr); | |
| return attr; | |
| } | |
| catch (IOException ioe) | |
| { | |
| throw new JarException("Section should start with a Name header: " | |
| + ioe.getMessage()); | |
| } | |
| } | |
| private static String expectHeader(String header, BufferedReader br, String s) | |
| throws IOException | |
| { | |
| try | |
| { | |
| String name = s.substring(0, header.length() + 1); | |
| if (name.equalsIgnoreCase(header + ":")) | |
| { | |
| String value_start = s.substring(header.length() + 2); | |
| return readHeaderValue(value_start, br); | |
| } | |
| } | |
| catch (IndexOutOfBoundsException ignored) | |
| { | |
| } | |
| // If we arrive here, something went wrong | |
| throw new JarException("unexpected '" + s + "'"); | |
| } | |
| // Methods for writing Manifest files to an OutputStream -------------------- | |
| public static void | |
| writeMFManifest(Attributes attr, Map entries, OutputStream stream) | |
| throws IOException | |
| { | |
| BufferedOutputStream out = stream instanceof BufferedOutputStream | |
| ? (BufferedOutputStream) stream | |
| : new BufferedOutputStream(stream, 4096); | |
| writeVersionInfo(attr, out); | |
| Iterator i; | |
| Map.Entry e; | |
| for (i = attr.entrySet().iterator(); i.hasNext();) | |
| { | |
| e = (Map.Entry) i.next(); | |
| // Don't print the manifest version again | |
| if (! Name.MANIFEST_VERSION.equals(e.getKey())) | |
| writeAttributeEntry(e, out); | |
| } | |
| out.write(CRLF); | |
| Iterator j; | |
| for (i = entries.entrySet().iterator(); i.hasNext();) | |
| { | |
| e = (Map.Entry) i.next(); | |
| writeHeader(NAME, e.getKey().toString(), out); | |
| Attributes eAttr = (Attributes) e.getValue(); | |
| for (j = eAttr.entrySet().iterator(); j.hasNext();) | |
| { | |
| Map.Entry e2 = (Map.Entry) j.next(); | |
| writeAttributeEntry(e2, out); | |
| } | |
| out.write(CRLF); | |
| } | |
| out.flush(); | |
| } | |
| public static void | |
| writeSFManifest(Attributes attr, Map entries, OutputStream stream) | |
| throws IOException | |
| { | |
| BufferedOutputStream out = stream instanceof BufferedOutputStream | |
| ? (BufferedOutputStream) stream | |
| : new BufferedOutputStream(stream, 4096); | |
| writeHeader(Name.SIGNATURE_VERSION.toString(), DEFAULT_SF_VERSION, out); | |
| writeHeader(CREATED_BY.toString(), CREATOR, out); | |
| Iterator i; | |
| Map.Entry e; | |
| for (i = attr.entrySet().iterator(); i.hasNext();) | |
| { | |
| e = (Map.Entry) i.next(); | |
| Name name = (Name) e.getKey(); | |
| if (Name.SIGNATURE_VERSION.equals(name) || CREATED_BY.equals(name)) | |
| continue; | |
| writeHeader(name.toString(), (String) e.getValue(), out); | |
| } | |
| out.write(CRLF); | |
| Iterator j; | |
| for (i = entries.entrySet().iterator(); i.hasNext();) | |
| { | |
| e = (Map.Entry) i.next(); | |
| writeHeader(NAME, e.getKey().toString(), out); | |
| Attributes eAttr = (Attributes) e.getValue(); | |
| for (j = eAttr.entrySet().iterator(); j.hasNext();) | |
| { | |
| Map.Entry e2 = (Map.Entry) j.next(); | |
| writeHeader(e2.getKey().toString(), (String) e2.getValue(), out); | |
| } | |
| out.write(CRLF); | |
| } | |
| out.flush(); | |
| } | |
| private static void writeVersionInfo(Attributes attr, OutputStream out) | |
| throws IOException | |
| { | |
| // First check if there is already a version attribute set | |
| String version = attr.getValue(Name.MANIFEST_VERSION); | |
| if (version == null) | |
| version = DEFAULT_MF_VERSION; | |
| writeHeader(Name.MANIFEST_VERSION.toString(), version, out); | |
| } | |
| private static void writeAttributeEntry(Map.Entry entry, OutputStream out) | |
| throws IOException | |
| { | |
| String name = entry.getKey().toString(); | |
| String value = entry.getValue().toString(); | |
| if (name.equalsIgnoreCase(NAME)) | |
| throw new JarException("Attributes cannot be called 'Name'"); | |
| if (name.startsWith("From")) | |
| throw new JarException("Header cannot start with the four letters 'From'" | |
| + name); | |
| writeHeader(name, value, out); | |
| } | |
| /** | |
| * The basic method for writing <code>Mainfest</code> attributes. This | |
| * implementation respects the rule stated in the Jar Specification concerning | |
| * the maximum allowed line length; i.e. | |
| * | |
| * <pre> | |
| * No line may be longer than 72 bytes (not characters), in its UTF8-encoded | |
| * form. If a value would make the initial line longer than this, it should | |
| * be continued on extra lines (each starting with a single SPACE). | |
| * </pre> | |
| * | |
| * and | |
| * | |
| * <pre> | |
| * Because header names cannot be continued, the maximum length of a header | |
| * name is 70 bytes (there must be a colon and a SPACE after the name). | |
| * </pre> | |
| * | |
| * @param name the name of the attribute. | |
| * @param value the value of the attribute. | |
| * @param out the output stream to write the attribute's name/value pair to. | |
| * @throws IOException if an I/O related exception occurs during the process. | |
| */ | |
| private static void writeHeader(String name, String value, OutputStream out) | |
| throws IOException | |
| { | |
| String target = name + ": "; | |
| byte[] b = target.getBytes("UTF-8"); | |
| if (b.length > 72) | |
| throw new IOException("Attribute's name already longer than 70 bytes"); | |
| if (b.length == 72) | |
| { | |
| out.write(b); | |
| out.write(CRLF); | |
| target = " " + value; | |
| } | |
| else | |
| target = target + value; | |
| int n; | |
| while (true) | |
| { | |
| b = target.getBytes("UTF-8"); | |
| if (b.length < 73) | |
| { | |
| out.write(b); | |
| break; | |
| } | |
| // find an appropriate character position to break on | |
| n = 72; | |
| while (true) | |
| { | |
| b = target.substring(0, n).getBytes("UTF-8"); | |
| if (b.length < 73) | |
| break; | |
| n--; | |
| if (n < 1) | |
| throw new IOException("Header is unbreakable and longer than 72 bytes"); | |
| } | |
| out.write(b); | |
| out.write(CRLF); | |
| target = " " + target.substring(n); | |
| } | |
| out.write(CRLF); | |
| } | |
| } |