1 /* JarUtils.java -- Utility methods for reading/writing Manifest[-like] files
2    Copyright (C) 2006, 2007 Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 
39 package gnu.java.util.jar;
40 
41 import gnu.classpath.SystemProperties;
42 
43 import java.io.BufferedOutputStream;
44 import java.io.BufferedReader;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.InputStreamReader;
48 import java.io.OutputStream;
49 import java.util.Iterator;
50 import java.util.Map;
51 import java.util.jar.Attributes;
52 import java.util.jar.JarException;
53 import java.util.jar.Attributes.Name;
54 import java.util.logging.Logger;
55 
56 /**
57  * Utility methods for reading and writing JAR <i>Manifest</i> and
58  * <i>Manifest-like</i> files.
59  * <p>
60  * JAR-related files that resemble <i>Manifest</i> files are Signature files
61  * (with an <code>.SF</code> extension) found in signed JARs.
62  */
63 public abstract class JarUtils
64 {
65   // We used to log here, but this causes problems during bootstrap,
66   // and it didn't seem worthwhile to preserve this.  Still, this
67   // might be useful for debugging.
68   // private static final Logger log = Logger.getLogger(JarUtils.class.getName());
69   public static final String META_INF = "META-INF/";
70   public static final String DSA_SUFFIX = ".DSA";
71   public static final String SF_SUFFIX = ".SF";
72   public static final String NAME = "Name";
73 
74   /**
75    * The original string representation of the manifest version attribute name.
76    */
77   public static final String MANIFEST_VERSION = "Manifest-Version";
78 
79   /**
80    * The original string representation of the signature version attribute
81    * name.
82    */
83   public static final String SIGNATURE_VERSION = "Signature-Version";
84 
85   /** Platform-independent line-ending. */
86   public static final byte[] CRLF = new byte[] { 0x0D, 0x0A };
87   private static final String DEFAULT_MF_VERSION = "1.0";
88   private static final String DEFAULT_SF_VERSION = "1.0";
89   private static final Name CREATED_BY = new Name("Created-By");
90   private static final String CREATOR = SystemProperties.getProperty("java.version")
91                                         + " ("
92                                         + SystemProperties.getProperty("java.vendor")
93                                         + ")";
94 
95   // default 0-arguments constructor
96 
97   // Methods for reading Manifest files from InputStream ----------------------
98 
99   public static void
readMFManifest(Attributes attr, Map entries, InputStream in)100   readMFManifest(Attributes attr, Map entries, InputStream in)
101       throws IOException
102   {
103     BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
104     readMainSection(attr, br);
105     readIndividualSections(entries, br);
106   }
107 
108   public static void
readSFManifest(Attributes attr, Map entries, InputStream in)109   readSFManifest(Attributes attr, Map entries, InputStream in)
110       throws IOException
111   {
112     BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
113     String version_header = Name.SIGNATURE_VERSION.toString();
114     try
115       {
116         String version = expectHeader(version_header, br);
117         attr.putValue(SIGNATURE_VERSION, version);
118         // This may cause problems during VM bootstrap.
119         // if (! DEFAULT_SF_VERSION.equals(version))
120         //  log.warning("Unexpected version number: " + version
121         //              + ". Continue (but may fail later)");
122       }
123     catch (IOException ioe)
124       {
125         throw new JarException("Signature file MUST start with a "
126                                + version_header + ": " + ioe.getMessage());
127       }
128     read_attributes(attr, br);
129 
130     // read individual sections
131     String s = br.readLine();
132     while (s != null && s.length() > 0)
133       {
134         Attributes eAttr = readSectionName(s, br, entries);
135         read_attributes(eAttr, br);
136         s = br.readLine();
137       }
138   }
139 
readMainSection(Attributes attr, BufferedReader br)140   private static void readMainSection(Attributes attr, BufferedReader br)
141       throws IOException
142   {
143     // According to the spec we should actually call read_version_info() here.
144     read_attributes(attr, br);
145     // Explicitly set Manifest-Version attribute if not set in Main
146     // attributes of Manifest.
147     // XXX (rsn): why 0.0 and not 1.0?
148     if (attr.getValue(Name.MANIFEST_VERSION) == null)
149       attr.putValue(MANIFEST_VERSION, "0.0");
150   }
151 
readIndividualSections(Map entries, BufferedReader br)152   private static void readIndividualSections(Map entries, BufferedReader br)
153       throws IOException
154   {
155     String s = br.readLine();
156     while (s != null && (! s.equals("")))
157       {
158         Attributes attr = readSectionName(s, br, entries);
159         read_attributes(attr, br);
160         s = br.readLine();
161       }
162   }
163 
164   /**
165    * Pedantic method that requires the next attribute in the Manifest to be the
166    * "Manifest-Version". This follows the Manifest spec closely but reject some
167    * jar Manifest files out in the wild.
168    */
readVersionInfo(Attributes attr, BufferedReader br)169   private static void readVersionInfo(Attributes attr, BufferedReader br)
170       throws IOException
171   {
172     String version_header = Name.MANIFEST_VERSION.toString();
173     try
174       {
175         String value = expectHeader(version_header, br);
176         attr.putValue(MANIFEST_VERSION, value);
177       }
178     catch (IOException ioe)
179       {
180         throw new JarException("Manifest should start with a " + version_header
181                                + ": " + ioe.getMessage());
182       }
183   }
184 
expectHeader(String header, BufferedReader br)185   private static String expectHeader(String header, BufferedReader br)
186       throws IOException
187   {
188     String s = br.readLine();
189     if (s == null)
190       throw new JarException("unexpected end of file");
191 
192     return expectHeader(header, br, s);
193   }
194 
read_attributes(Attributes attr, BufferedReader br)195   private static void read_attributes(Attributes attr, BufferedReader br)
196       throws IOException
197   {
198     String s = br.readLine();
199     while (s != null && (! s.equals("")))
200       {
201         readAttribute(attr, s, br);
202         s = br.readLine();
203       }
204   }
205 
206   private static void
readAttribute(Attributes attr, String s, BufferedReader br)207   readAttribute(Attributes attr, String s, BufferedReader br) throws IOException
208   {
209     try
210       {
211         int colon = s.indexOf(": ");
212         String name = s.substring(0, colon);
213         String value_start = s.substring(colon + 2);
214         String value = readHeaderValue(value_start, br);
215         attr.putValue(name, value);
216       }
217     catch (IndexOutOfBoundsException iobe)
218       {
219         throw new JarException("Manifest contains a bad header: " + s);
220       }
221   }
222 
readHeaderValue(String s, BufferedReader br)223   private static String readHeaderValue(String s, BufferedReader br)
224       throws IOException
225   {
226     boolean try_next = true;
227     while (try_next)
228       {
229         // Lets see if there is something on the next line
230         br.mark(1);
231         if (br.read() == ' ')
232           s += br.readLine();
233         else
234           {
235             br.reset();
236             try_next = false;
237           }
238       }
239     return s;
240   }
241 
242   private static Attributes
readSectionName(String s, BufferedReader br, Map entries)243   readSectionName(String s, BufferedReader br, Map entries) throws JarException
244   {
245     try
246       {
247         String name = expectHeader(NAME, br, s);
248         Attributes attr = new Attributes();
249         entries.put(name, attr);
250         return attr;
251       }
252     catch (IOException ioe)
253       {
254         throw new JarException("Section should start with a Name header: "
255                                + ioe.getMessage());
256       }
257   }
258 
expectHeader(String header, BufferedReader br, String s)259   private static String expectHeader(String header, BufferedReader br, String s)
260       throws IOException
261   {
262     try
263       {
264         String name = s.substring(0, header.length() + 1);
265         if (name.equalsIgnoreCase(header + ":"))
266           {
267             String value_start = s.substring(header.length() + 2);
268             return readHeaderValue(value_start, br);
269           }
270       }
271     catch (IndexOutOfBoundsException ignored)
272       {
273       }
274     // If we arrive here, something went wrong
275     throw new JarException("unexpected '" + s + "'");
276   }
277 
278   // Methods for writing Manifest files to an OutputStream --------------------
279 
280   public static void
writeMFManifest(Attributes attr, Map entries, OutputStream stream)281   writeMFManifest(Attributes attr, Map entries, OutputStream stream)
282       throws IOException
283   {
284     BufferedOutputStream out = stream instanceof BufferedOutputStream
285                                ? (BufferedOutputStream) stream
286                                : new BufferedOutputStream(stream, 4096);
287     writeVersionInfo(attr, out);
288     Iterator i;
289     Map.Entry e;
290     for (i = attr.entrySet().iterator(); i.hasNext();)
291       {
292         e = (Map.Entry) i.next();
293         // Don't print the manifest version again
294         if (! Name.MANIFEST_VERSION.equals(e.getKey()))
295           writeAttributeEntry(e, out);
296       }
297     out.write(CRLF);
298 
299     Iterator j;
300     for (i = entries.entrySet().iterator(); i.hasNext();)
301       {
302         e = (Map.Entry) i.next();
303         writeHeader(NAME, e.getKey().toString(), out);
304         Attributes eAttr = (Attributes) e.getValue();
305         for (j = eAttr.entrySet().iterator(); j.hasNext();)
306           {
307             Map.Entry e2 = (Map.Entry) j.next();
308             writeAttributeEntry(e2, out);
309           }
310         out.write(CRLF);
311       }
312 
313     out.flush();
314   }
315 
316   public static void
writeSFManifest(Attributes attr, Map entries, OutputStream stream)317   writeSFManifest(Attributes attr, Map entries, OutputStream stream)
318       throws IOException
319   {
320     BufferedOutputStream out = stream instanceof BufferedOutputStream
321                                ? (BufferedOutputStream) stream
322                                : new BufferedOutputStream(stream, 4096);
323     writeHeader(Name.SIGNATURE_VERSION.toString(), DEFAULT_SF_VERSION, out);
324     writeHeader(CREATED_BY.toString(), CREATOR, out);
325     Iterator i;
326     Map.Entry e;
327     for (i = attr.entrySet().iterator(); i.hasNext();)
328       {
329         e = (Map.Entry) i.next();
330         Name name = (Name) e.getKey();
331         if (Name.SIGNATURE_VERSION.equals(name) || CREATED_BY.equals(name))
332           continue;
333 
334         writeHeader(name.toString(), (String) e.getValue(), out);
335       }
336     out.write(CRLF);
337 
338     Iterator j;
339     for (i = entries.entrySet().iterator(); i.hasNext();)
340       {
341         e = (Map.Entry) i.next();
342         writeHeader(NAME, e.getKey().toString(), out);
343         Attributes eAttr = (Attributes) e.getValue();
344         for (j = eAttr.entrySet().iterator(); j.hasNext();)
345           {
346             Map.Entry e2 = (Map.Entry) j.next();
347             writeHeader(e2.getKey().toString(), (String) e2.getValue(), out);
348           }
349         out.write(CRLF);
350       }
351 
352     out.flush();
353   }
354 
writeVersionInfo(Attributes attr, OutputStream out)355   private static void writeVersionInfo(Attributes attr, OutputStream out)
356       throws IOException
357   {
358     // First check if there is already a version attribute set
359     String version = attr.getValue(Name.MANIFEST_VERSION);
360     if (version == null)
361       version = DEFAULT_MF_VERSION;
362 
363     writeHeader(Name.MANIFEST_VERSION.toString(), version, out);
364   }
365 
writeAttributeEntry(Map.Entry entry, OutputStream out)366   private static void writeAttributeEntry(Map.Entry entry, OutputStream out)
367       throws IOException
368   {
369     String name = entry.getKey().toString();
370     String value = entry.getValue().toString();
371     if (name.equalsIgnoreCase(NAME))
372       throw new JarException("Attributes cannot be called 'Name'");
373 
374     if (name.startsWith("From"))
375       throw new JarException("Header cannot start with the four letters 'From'"
376                              + name);
377 
378     writeHeader(name, value, out);
379   }
380 
381   /**
382    * The basic method for writing <code>Mainfest</code> attributes. This
383    * implementation respects the rule stated in the Jar Specification concerning
384    * the maximum allowed line length; i.e.
385    *
386    * <pre>
387    * No line may be longer than 72 bytes (not characters), in its UTF8-encoded
388    * form. If a value would make the initial line longer than this, it should
389    * be continued on extra lines (each starting with a single SPACE).
390    * </pre>
391    *
392    * and
393    *
394    * <pre>
395    * Because header names cannot be continued, the maximum length of a header
396    * name is 70 bytes (there must be a colon and a SPACE after the name).
397    * </pre>
398    *
399    * @param name the name of the attribute.
400    * @param value the value of the attribute.
401    * @param out the output stream to write the attribute's name/value pair to.
402    * @throws IOException if an I/O related exception occurs during the process.
403    */
writeHeader(String name, String value, OutputStream out)404   private static void writeHeader(String name, String value, OutputStream out)
405       throws IOException
406   {
407     String target = name + ": ";
408     byte[] b = target.getBytes("UTF-8");
409     if (b.length > 72)
410       throw new IOException("Attribute's name already longer than 70 bytes");
411 
412     if (b.length == 72)
413       {
414         out.write(b);
415         out.write(CRLF);
416         target = " " + value;
417       }
418     else
419       target = target + value;
420 
421     int n;
422     while (true)
423       {
424         b = target.getBytes("UTF-8");
425         if (b.length < 73)
426           {
427             out.write(b);
428             break;
429           }
430 
431         // find an appropriate character position to break on
432         n = 72;
433         while (true)
434           {
435             b = target.substring(0, n).getBytes("UTF-8");
436             if (b.length < 73)
437               break;
438 
439             n--;
440             if (n < 1)
441               throw new IOException("Header is unbreakable and longer than 72 bytes");
442           }
443 
444         out.write(b);
445         out.write(CRLF);
446         target = " " + target.substring(n);
447       }
448 
449     out.write(CRLF);
450   }
451 }
452