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