package org.gjt.cuspy; import java.io.InputStream; import java.io.InputStreamReader; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.OutputStreamWriter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import java.net.URL; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; /** * Distribute your work as a self-extracting jar file by including one file, * JarX.class, that also safely converts text files to the receiver's encoding * and newline conventions, and adds less than 7 kB to your jar. *
* A self-extracting file is handy if your recipient might have a * Java runtime environment but not the jar tool. * The text conversion offered by JarX is useful if your distribution will * include text files, source, documentation, scripts, etc., and your recipients * have platforms with different newline conventions. *
* It's important to apply such transformations only to the files * within the archive that are actually known to contain text. * Passing binary data or class files through character and newline * transformations will corrupt them. *
* One problem, though not the fatal one, with this scheme is that there is no * single convention for newlines inside the zip file. Instead, files are * stored just as they are found on the source system, and a code indicating the * source operating system is stored in the archive. The receiving unzip * program must interpret the code and know what newline convention that * operating system used, in order to convert newlines appropriately. *
* The fatal flaw, however, has to do with the way the text/binary * bit gets set in the first place. While building the archive, the common zip * programs look at statistical properties of byte frequencies in the input, * and set the text bit for any entry that looks like it might be text! If a * binary file happens to contain an unlucky sequence of bytes, it will be * flagged as text and then silently corrupted by any unzip program that honors * the text bit. That can happen, and has happened, to class files in zip * archives if the recipient uses unzip -a, and causes significant misery if * the package is widely distributed. *
* As of JDK 1.3, Sun has extended the
*
*Jar File Specification to allow a Content-Type in the
* Manifest for each jar entry. The value of Content-Type is a
*MIME
* type, and with this a developer can specify exactly which entries in a
* jar should be treated as text. The question of a standard representation
* for newlines inside the jar is settled, because
* [RFC2046 section 4.1.1]
* establishes a canonical line break representation for all subtypes of the
* text MIME type. Therefore, correct translation of line breaks from any
* platform to any platform can be achieved if a jar-building program just
* converts from its local convention to the canonical CRLF form, and a jar
* extraction program just converts the canonical to its own local form. Neither
* program needs to know anything about the other environment.
* Finally, the charset parameter of the text type
* allows explicit specification of the character encoding used in a jar entry,
* and the extracting program can automatically convert into the encoding used
* on the local system. (But see Call to action below.)
*
Content-Type entries in a Manifest were introduced in Java 1.3
* but are compatible with earlier jar specifications; a jar file containing
* such entries can be processed without any trouble by any jar tool compliant
* with the old or new standard. However, there is not yet a full jar tool
* available that will honor the content types and do automatic transformation
* of text entries. To fill the need until that functionality is added to the
* widely-available jar tools, JarX is available now.
*
* JarX.Build produces a jar, working from a manifest file prepared by the
* developer. Entries with any text type will be translated from
* the local encoding into the specified charset if given, and
* entries with the specific type text/plain will have their line
* endings converted to the CRLF canonical form. Line endings are left alone
* for all other subtypes of text, but this decision is open to
* comment.
*
* The file produced by JarX.Build is a fully compliant jar and can be unpacked
* by any jar or unzip tool, but current tools will not automatically convert
* the text files to the local conventions. By including the single class file
* JarX.class in the jar, a developer produces a self-extracting
* archive that can be executed to unpack itself on any Java 1.6 or later
* virtual machine, performing all automatic conversions and requiring no jar
* tool at all.
*
Name: entry for every file
* to be included in the jar. JarX.Build archives only the files named in
* the manifest. Be sure to include Manifest-Version: 1.0 as
* the first line of the manifest; JarX.Build does not do it for you. To make
* the jar self-extracting, make the next lineMain-Class: org.gjt.cuspy.JarXName: entry for org/gjt/cuspy/JarX.class.
*
* Add an appropriate Content-Type: line after the
* Name: line for every entry that needs one. JarX itself only
* distinguishes the text types from nontext (everything else),
* and treats a missing Content-Type: as nontext, so for purposes
* of JarX you only need to add content types for text files. For other
* purposes you may wish to include the types of other entries as well.
* In the simplest case, just omit content types for your non-text files,
* and add Content-Type: text/plain; charset=UTF-8 for files that
* you want auto-converted. Then give the command
* java org.gjt.cuspy.JarX$Build foo.jar manifest
if
* manifest is the name of your prepared manifest file and
* foo.jar names the jar you want to create.
* The order of files in the jar will be the order of their names in the
* manifest.
*
java -jar foo.jar is all it takes
* to extract a jar. The Main-Class entry in the manifest
* identifies the entry point of JarX so it does not need to be specified.
** JarX *
* Error handling is roughly nonexistent. JarX is careful to avoid silent * corruption of data, even verifying that all character encoding calls are * successful, but makes no attempt to be graceful about errors or surprises. * If something doesn't work the likely result is a one line message and abrupt * exit, or an uncaught exception and stack trace. *
* The coding style is a little contrived just to arrange it so JarX.class is
* the only file needed in the jar to make it self-extracting. In particular
* the JarX class is also written to serve as the class of tokens returned by
* the structured-field-body lexer, to avoid introducing a second class. Weird,
* perhaps, but harmless weird.
*@author Chapman Flack
*@version $Id$
*/
public class JarX {
/**How to treat the entry being processed: bytes, characters, lines.
* Used only in the JarX instance created by main(). Set by classify().
* Only the exact String instances BYTES, CHARACTERS, LINES are to be used.
*/
protected String treatment;
protected static final String BYTES = "bytes";
protected static final String CHARACTERS = "characters";
protected static final String LINES = "lines";
/**Charset (in archive) of the entry being processed.
* Used only in the JarX instance created by main(). Set by classify().
*/
protected Charset archiveCharset;
/**Charset when unpacked of the entry being processed.
* Used only in the JarX instance created by main(). Set by classify().
*/
protected Charset unpackedCharset;
/**Read permission to be set on the file.
* Only the final Strings NONE, OWNER, or ALL are to be used, or null, in
* which case no explicit setting is made and the OS defaults apply.
*/
protected String readPermission;
/**Write permission to be set on the file.
* Only the final Strings NONE, OWNER, or ALL are to be used, or null, in
* which case no explicit setting is made and the OS defaults apply.
*/
protected String writePermission;
/**Execute permission to be set on the file.
* Only the final Strings NONE, OWNER, or ALL are to be used, or null, in
* which case no explicit setting is made and the OS defaults apply.
*/
protected String executePermission;
protected static final String NONE = "none";
protected static final String OWNER = "owner";
protected static final String ALL = "all";
/**As for treatment, but set from main attributes (or BYTES if not present).*/
protected String defaultTreatment = BYTES;
/**As for archiveCharset, but set from main attributes (default UTF-8).*/
protected Charset defaultArchiveCharset = Charset.forName( "UTF-8");
/**As for unpackedCharset, but set from main attributes or platform default.*/
protected Charset defaultUnpackedCharset = Charset.defaultCharset();
/**As for readPermission but set from main attributes, null if not present.*/
protected String defaultReadPermission;
/**As for writePermission but set from main attributes, null if not present.*/
protected String defaultWritePermission;
/**As for executePermission but set from main attributes, null if not present.
*/
protected String defaultExecutePermission;
/**Script engine to run the name resolver script, if any.*/
protected ScriptEngine resolverEngine;
/**The name resolver script, if any.*/
protected String resolverScript;
/**Attribute name for specifying the in-archive charset.
* The Java powers that be didn't go for
*
*Bug #4310708 so there needs to be a dedicated manifest key for this
* (though JarX will still honor ;charset= on the Content-Type too).
*/
public final Attributes.Name ARCHIVE_CHARSET =
new Attributes.Name( "_JarX_CharsetInArchive");
/**Attribute name for specifying the when-unpacked charset.
* This was not in the original JarX; the platform default was always used,
* and still is if this attribute is not present.
*/
public final Attributes.Name UNPACKED_CHARSET =
new Attributes.Name( "_JarX_CharsetWhenUnpacked");
/**Permissions (only as supported in java.io.File for SE 6)
* spec *(, spec) where spec is action=whom, action is read, write, or
* execute, and whom is none, owner, or all.
*/
public final Attributes.Name PERMISSIONS =
new Attributes.Name( "_JarX_Permissions");
/** Main attribute to specify a JSR223 script to control extracted names. */
public final Attributes.Name PATHRESOLVER =
new Attributes.Name( "_JarX_PathResolver");
/**Main attributes saved from the manifest (which must be seen early).*/
protected Attributes mainAttributes;
/**Token type, when JarX objects are used to return content type tokens*/
public short type;
/**Token text when JarX objects are used to return content type tokens*/
public String value;
/**Token types from the structured field body lexer defined in
*RFC822
* as modified in
*RFC2045.
* Also state numbers for the automaton in
* {@link #structuredFieldBody(String,int) structuredFieldBody}.
*/
public static final short ATOM = 5;
public static final short COMMENT = 4;
public static final short DOMAINLITERAL = 3;
public static final short QUOTEDSTRING = 2;
public static final short TSPECIAL = 1;
static final short START = 0;
/**True if this JarX object represents a token of one of the given types.
* @param type allowable types
* @return as titled
*/
public boolean is( short... type) {
for ( short t : type )
if ( t == this.type )
return true;
return false;
}
/**True if this JarX object represents a token of one of the given types
* and its value equals the given string.
* @param value string value for comparison
* @param type allowable types
* @return as titled
*/
public boolean holds( String value, short... type) {
return is( type) && value.equals( this.value);
}
/**True if this JarX object represents a token of one of the given types
* and its value equals the given string, case-insensitively.
* @param value string value for comparison
* @param type allowable types
* @return as titled
*/
public boolean holdsIgnoreCase( String value, short... type) {
return is( type) && value.equalsIgnoreCase( this.value);
}
/**Name of the JarX class file as stored in the jar*/
public static final String me
= JarX.class.getName().replace('.', '/') + ".class";
/**Name of the manifest file as stored in the jar*/
public static final String manifestName = "META-INF/MANIFEST.MF";
/**The (fixed) encoding used for manifest content*/
public static final String manifestCode = "UTF-8";
/**The entry point for extracting.
*@param args argument list
*@throws Exception if anything doesn't work, punt
*/
public static void main( String[] args) throws Exception {
JarX e = new JarX();
if ( args.length > 0 ) {
System.err.println( "usage: java -jar filename.jar");
System.exit( 1);
}
e.extract();
}
/**Find the jar I was loaded from and extract all entries except my own
* class file.
*@throws Exception if anything doesn't work, punt
*/
public void extract() throws Exception {
URL jarURL =
this.getClass().getProtectionDomain().getCodeSource().getLocation();
InputStream is = jarURL.openStream();
JarInputStream jis = new JarInputStream( is);
Manifest mf = null;
for ( JarEntry je;; ) {
je = jis.getNextJarEntry();
if ( je == null )
break;
if ( null == mf ) {
mf = jis.getManifest();
if ( null != mf )
setDefaults( mf.getMainAttributes());
}
if ( ! je.getName().equals( me) )
extract( je, jis);
jis.closeEntry();
}
jis.close();
}
/**Examine the main attributes to set any defaults.
* Includes loading the required script engine if a name resolver script
* is given.
* @param mainAttributes as obtained from the manifest
*/
public void setDefaults( Attributes mainAttributes) {
this.mainAttributes = mainAttributes;
classify( mainAttributes, false);
defaultTreatment = treatment;
defaultArchiveCharset = archiveCharset;
defaultUnpackedCharset = unpackedCharset;
defaultReadPermission = readPermission;
defaultWritePermission = writePermission;
defaultExecutePermission = executePermission;
if ( null == mainAttributes )
return;
String v = mainAttributes.getValue( PATHRESOLVER);
if ( null == v )
return;
JarX[] toks = structuredFieldBody( v, 0);
if ( toks.length < 4
|| ! toks[0].is( ATOM)
|| ! toks[1].holds("/", TSPECIAL)
|| ! toks[2].is( ATOM)
|| ! toks[3].is( QUOTEDSTRING) ) {
System.err.printf( "Malformed name resolver attribute: %s\n", v);
System.exit( 1);
}
String mimetype = toks[0].value + "/" + toks[2].value;
StringBuilder script = new StringBuilder( toks[3].value);
int i = 4;
while ( i < toks.length ) {
if ( toks[i].holds( "/", TSPECIAL) )
script.append( '\n');
else if ( toks[i].is( QUOTEDSTRING) )
script.append( toks[i].value);
else
break;
++i;
}
if ( i < toks.length ) {
System.err.printf( "Malformed name resolver attribute: %s\n", v);
System.exit( 1);
}
ScriptEngineManager mgr = new ScriptEngineManager();
resolverEngine = mgr.getEngineByMimeType( mimetype);
if ( null == resolverEngine ) {
System.err.printf( "No script engine found for %s\n", mimetype);
System.exit( 1);
}
resolverEngine.put( "properties", System.getProperties());
resolverScript = script.toString();
}
/**Set instance variables for text/binary and permissions treatment
* according to the passed Attributes.
* @param atts Usually a per-entry attribute set, but {@code classify} is
* also called by {@code setDefaults} to parse the main attributes.
* @param lazy In the usual case, as soon as an entry is classified as
* non-text, {@code classify} can return without looking for charset
* information. When called by {@code setDefaults}, however, laziness is not
* appropriate.
*/
public void classify( Attributes atts, boolean lazy) {
treatment = defaultTreatment;
archiveCharset = defaultArchiveCharset;
unpackedCharset = defaultUnpackedCharset;
readPermission = defaultReadPermission;
writePermission = defaultWritePermission;
executePermission = defaultExecutePermission;
if ( null == atts )
return;
String v = atts.getValue( PERMISSIONS);
if ( null != v ) {
String r = null;
String w = null;
String x = null;
JarX[] toks = structuredFieldBody( v, 0);
int i = 0;
while ( i + 2 < toks.length ) {
if ( ! toks[i].is( ATOM) || ! toks[i+1].holds( "=", TSPECIAL) )
break;
if ( ! toks[i+2].is( ATOM) )
break;
String p = toks[i].value;
String noa = toks[i+2].value;
if ( NONE.equalsIgnoreCase( noa) )
noa = NONE;
else if ( OWNER.equalsIgnoreCase( noa) )
noa = OWNER;
else if ( ALL.equalsIgnoreCase( noa) )
noa = ALL;
else
break;
if ( "read".equalsIgnoreCase( p) && null == r )
r = noa;
else if ( "write".equalsIgnoreCase( p) && null == w )
w = noa;
else if ( "execute".equalsIgnoreCase( p) && null == x )
x = noa;
else
break;
i += 3;
if ( i+3 < toks.length && toks[i].holds( ",", TSPECIAL) )
++i;
}
if ( i < toks.length ) {
System.err.printf( "Malformed permissions attribute: %s\n", v);
System.exit( 1);
}
if ( null != r )
readPermission = r;
if ( null != w )
writePermission = w;
if ( null != x )
executePermission = x;
}
boolean archiveCharsetFound = false;
v = atts.getValue( Attributes.Name.CONTENT_TYPE);
if ( null != v ) {
JarX[] type = structuredFieldBody( v, 0);
if ( type[0].holdsIgnoreCase( "text", ATOM)
&& type[1].holds( "/", TSPECIAL) ) {
treatment = type[2].holdsIgnoreCase( "plain", ATOM)? LINES : CHARACTERS;
archiveCharsetFound = archiveCharsetFromType( type);
}
}
if ( BYTES == treatment && lazy )
return;
if ( ! archiveCharsetFound ) {
v = atts.getValue( ARCHIVE_CHARSET);
if ( null != v )
archiveCharset = Charset.forName( v);
}
v = atts.getValue( UNPACKED_CHARSET);
if ( null != v )
unpackedCharset = Charset.forName( v);
}
/**Parse a Content-Type for any {@code charset} parameter.
* @param type tokenized Content-Type value
* @return true if the Content-Type specified a charset
*/
protected boolean archiveCharsetFromType( JarX[] type) {
String charset = null;
int i = 3;
while ( i < type.length ) {
if ( ! type[i].holds( ";", TSPECIAL) )
break;
if ( type[++i].holdsIgnoreCase( "charset", ATOM) ) {
if ( ! type[++i].holds( "=", TSPECIAL) )
break;
if ( ! type[++i].is( ATOM, QUOTEDSTRING) )
break;
charset = type[i].value;
break;
}
if ( ! type[++i].holds( "=", TSPECIAL) )
break;
if ( ! type[++i].is( ATOM, QUOTEDSTRING) )
break;
++i;
}
if ( null != charset ) {
archiveCharset = Charset.forName( charset);
return true;
}
if ( i < type.length ) {
System.err.println( "Malformed Content-Type specification!");
System.exit( 1);
}
return false;
}
/**Extract a single entry, performing any appropriate conversion
*@param je JarEntry for the current entry
*@param is InputStream with the current entry content
*@throws IOException for any problem involving I/O
*@throws ScriptException for any problem involving the script engine
*/
public void extract( JarEntry je, InputStream is)
throws IOException, ScriptException {
classify( je.getAttributes(), true);
String orig = je.getName();
String s = orig;
if ( File.separatorChar != '/' )
s = s.replace( '/', File.separatorChar);
if ( null != resolverScript ) {
resolverEngine.put( "storedPath", orig);
resolverEngine.put( "platformPath", s);
resolverEngine.put( "computedPath", s);
resolverEngine.eval( resolverScript);
s = (String)resolverEngine.get( "computedPath");
}
System.err.print( s + " ");
File f = new File( s);
if ( je.isDirectory() ) {
if ( f.isDirectory() || f.mkdirs() )
System.err.println();
else
System.err.println( "FAILED!");
return;
}
OutputStream os;
File tmpf;
File d = f.getParentFile();
if ( null == d )
d = new File( System.getProperty( "user.dir"));
try {
tmpf = File.createTempFile( f.getName(), ".tmp", d);
}
catch ( IOException e ) {
if ( ! d.mkdirs() )
throw e;
tmpf = File.createTempFile( f.getName(), ".tmp", d);
}
os = new FileOutputStream( tmpf);
if ( null != readPermission ) {
if ( ALL == readPermission )
tmpf.setReadable( true, false);
else {
tmpf.setReadable( false, false);
if ( OWNER == readPermission )
tmpf.setReadable( true, true);
}
}
if ( null != writePermission ) {
if ( ALL == writePermission )
tmpf.setWritable( true, false);
else {
tmpf.setWritable( false, false);
tmpf.setWritable( true, true); /* will when done writing */
}
}
shovel( is, os);
os.close();
if ( NONE == writePermission )
tmpf.setWritable( false, false);
if ( null != executePermission ) {
if ( ALL == executePermission )
tmpf.setExecutable( true, false);
else {
tmpf.setExecutable( false, false);
if ( OWNER == executePermission )
tmpf.setExecutable( true, true);
}
}
if ( ! tmpf.renameTo( f) ) {
if ( ! f.delete() || ! tmpf.renameTo( f) )
System.err.println( "RENAME FAILED!");
}
}
/**Copy content from an input to an output stream until end.
* Whether the content is shoveled as bytes, characters, or lines will be
* determined by instance variables that have been set by calling
* {@link #classify(Attributes,boolean) classify} before calling this method.
*@param is source of input
*@param os destination for output
*@throws IOException for any problem involving I/O
*/
public void shovel( InputStream is, OutputStream os) throws IOException {
if ( BYTES == treatment )
shovelBytes( is, os);
else
shovelText( is, os);
}
/**Copy bytes from an input to an output stream until end.
* No character encoding or newline conversion applies.
*@param is source of input
*@param os destination for output
*@throws IOException for any problem involving I/O
*/
public static void shovelBytes( InputStream is, OutputStream os)
throws IOException {
byte[] buf = new byte [ 1024 ];
int got;
for ( ;; ) {
got = is.read( buf, 0, buf.length);
if ( got == -1 )
break;
os.write( buf, 0, got);
}
System.err.println( "as bytes");
}
/**Copy text from an input to an output stream until end.
* Determines the encoding transformation to use (based on the
* charset content-type parameter) and whether to copy as
* lines (with newline conversion) or unmolested characters.
* text/plain is copied as lines, all other text subtypes
* as characters.
*@param is source of input
*@param os destination of output
*@throws IOException for any problem involving I/O
*/
public void
shovelText( InputStream is, OutputStream os)
throws IOException {
if ( LINES == treatment )
shovelLines( is, os);
else
shovelChars( is, os);
}
/**Copy lines of text from an input from an output stream, applying
* the specified character encoding and translating newlines.
* This method handles the extracting case, where the named encoding is
* associated with the input stream (jar) and the platform default encoding
* with the output (local file), and the local line.separator is used to
* separate lines on the output.
* Overridden in
* {@link JarX.Build#shovelLines(InputStream,OutputStream) build} to do
* the reverse when building a jar.
* To avoid silent corruption of data, this method verifies that all
* characters from the jar are successfully converted to the local platform's
* encoding.
*@param is the source of input
*@param os destination for output
*@throws IOException for any problem involving I/O
*/
public void
shovelLines( InputStream is, OutputStream os)
throws IOException {
InputStreamReader isr =
new InputStreamReader( is, archiveCharset.newDecoder());
BufferedReader br = new BufferedReader( isr);
OutputStreamWriter osw =
new OutputStreamWriter( os, unpackedCharset.newEncoder());
BufferedWriter bw = new BufferedWriter( osw);
String s;
for ( ;; ) {
s = br.readLine();
if ( s == null )
break;
bw.write( s);
bw.newLine();
}
bw.flush();
osw.flush();
System.err.printf( "as lines (%s)\n", describeTranscoding(isr, osw));
}
/**Copy characters of text from an input from an output stream,
* applying the specified character encoding but not translating newlines.
* This method handles the extracting case, where the named encoding is
* associated with the input stream (jar) and the platform default encoding
* with the output (local file).
* Overridden in
* {@link Build#shovelChars(InputStream,OutputStream) build} to do
* the reverse when building a jar.
* To avoid silent corruption of data, this method verifies that all
* characters from the jar are successfully converted to the local platform's
* encoding.
*@param is the source of input
*@param os destination for output
*@throws IOException for any problem involving I/O
*/
public void
shovelChars( InputStream is, OutputStream os)
throws IOException {
InputStreamReader isr =
new InputStreamReader( is, archiveCharset.newDecoder());
OutputStreamWriter osw =
new OutputStreamWriter( os, unpackedCharset.newEncoder());
char[] c = new char [ 1024 ];
int got;
for ( ;; ) {
got = isr.read( c, 0, c.length);
if ( got == -1 )
break;
osw.write( c, 0, got);
}
osw.flush();
System.err.printf( "as characters (%s)\n", describeTranscoding(isr, osw));
}
public String describeTranscoding(
InputStreamReader isr, OutputStreamWriter osw) {
String ie = isr.getEncoding();
String oe = osw.getEncoding();
if ( ie.equals( oe) )
return ie;
return ie + " -> " + oe;
}
/**Public constructor for an application using JarX to unpack jars.*/
public JarX() { }
/**Constructor for JarX objects used as tokens returned by the lexer.
*@param t the type of this token
*@param v the corresponding text (with delimiters removed and backslashes
* resolved for quoted strings, domain text, and comments)
*/
protected JarX( short t, String v) { type = t; value = v; }
/**Lexical analyzer for structured field bodies as described in
*RFC822
* and modified in
*RFC2045.
* Comments are processed and stored in tokens that are, at the last
* minute, excluded from the returned token list; only two lines would need
* to be changed to use this lexer in an application that wanted comments
* returned.
*@param field a header field
*@param off offset to the start of the structured field body
* (skip the field name and colon)
*@return An array of {@link #JarX(short,String) tokens} with any
* COMMENT tokens (for JarX purposes) excluded
*/
public static JarX[] structuredFieldBody( String field, int off) {
char[] buf = new char [ field.length() - off ];
field.getChars( off, off + buf.length, buf, 0);
int beg = 0, end = -1, la;
int commentDepth = 0;
short state = START;
short lastState = state;
boolean bashed = false;
ArrayListName: and a Content-Type
* attribute.
*@param r BufferedReader already open on the manifest
*@return true if there is another section to read, false if the end of the
* manifest has been reached
*@throws IOException if the manifest can't be read
*/
public boolean section( BufferedReader r)
throws IOException {
String field;
String front;
String name = null;
Attributes atts = new Attributes();
boolean gotany = false;
for ( ;; ) {
field = header( r);
if ( field == null || 0 == field.length() )
break;
gotany = true;
int i = field.indexOf( ": ");
if ( i < 1 ) {
System.err.printf( "Malformed line in manifest: %s\n", field);
System.exit( 1);
}
front = field.substring(0, i);
field = field.substring(i+2);
if ( front.equalsIgnoreCase( "Name") ) {
if ( name == null )
name = field;
else
System.err.println(
"Warning: name attribute repeated within a section, ignored.");
continue;
}
atts.putValue( front, field);
}
if ( ! gotany )
return null != field;
if ( null == name ) {
if ( null != mainAttributes ) {
System.err.println(
"Main attributes followed by another nameless section");
System.exit( 1);
}
setDefaults( atts);
}
else
store( name, atts);
return null != field;
}
/**Buffer used between calls to {@link #header(BufferedReader) header}.*/
String nextManifestLine = null;
/**Return one header line (complete after RFC822 continuation unfolding).
* Note: The Jar specification says it is "inspired by"
* RFC822, but the folding rule differs. RFC822 allows
* "linear whitespace" (i.e. space or tab) to start the continuation line,
* and the LWSP remains in the line (RFC822 lines are only supposed
* to be folded at places LWSP can appear). A jar manifest line continuation
* can only begin with a space, and the space is eaten; Java's
* manifest writer can arbitrarily fold in the middle of anything.
*@param r BufferedReader to read from
*@return the line read, or null at end of input
*@throws IOException if the input cannot be read
*/
public String header( BufferedReader r) throws IOException {
if ( nextManifestLine == null )
nextManifestLine = r.readLine();
String line = nextManifestLine;
for ( ;; ) {
nextManifestLine = r.readLine();
if ( nextManifestLine == null
|| ! nextManifestLine.startsWith( " ") )
break;
line += nextManifestLine.substring(1);
}
return line;
}
}
}