package er.woinstaller.archiver; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.tools.bzip2.CBZip2InputStream; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import er.woinstaller.io.BoundedInputStream; public class XarFile { private static final long XAR_HEADER_MAGIC = 0x78617221; private static final int XAR_HEADER_SIZE = 28; private static final String[] XAR_CKSUM = new String[] { "NONE", "SHA1", "MD5" }; private static final int BYTE_MASK = 0xff; private final byte[] byte2 = new byte[2]; private final byte[] byte4 = new byte[4]; private final byte[] byte8 = new byte[8]; private final Map<String, XarEntry> entries = new HashMap<String, XarEntry>(); private File file; private XarHeader header; private XarToc toc; private InputStream inputStream; private InputStream lastInputStream; private long currentOffset = 0; private class XarHeader { private static final int SHORT_MASK = 0xffff; public long magic; public int size; public int version; public BigInteger tocLengthCompressed; public BigInteger tocLengthUncompressed; public long checksumAlgorithm; protected XarHeader() throws IOException { magic = readUint32(); size = readUint16(); version = readUint16(); tocLengthCompressed = readUint64(); tocLengthUncompressed = readUint64(); checksumAlgorithm = readUint32(); } @SuppressWarnings("unused") public void dumpHeader() { System.out.println("\nmagic:\t\t\t 0x"+ Long.toHexString((magic >> Short.SIZE & SHORT_MASK)) + Long.toHexString(magic & SHORT_MASK) + " " + ((magic == XAR_HEADER_MAGIC)?"(OK)":"(BAD)")); System.out.println("size:\t\t\t "+size); System.out.println("version:\t\t "+version); System.out.println("Compressed TOC length:\t "+tocLengthCompressed); System.out.println("Uncompressed TOC length: "+tocLengthUncompressed); System.out.println("Checksum algorithm:\t "+checksumAlgorithm + " ("+getCksumName()+")"); } } private class XarToc { private static final int BUFFER_SIZE = 255; private final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); private DocumentBuilder builder; private Document doc; private String data; protected XarToc() throws IOException { try { data = readToc(); builder = factory.newDocumentBuilder(); doc = builder.parse(new InputSource(new StringReader(data))); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SAXException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private String readToc() throws IOException { InputStream inflate = new InflaterInputStream(new BoundedInputStream(inputStream, 0, header.tocLengthCompressed.longValue())); try { byte[] buffer = new byte[BUFFER_SIZE]; BigInteger length = new BigInteger(header.tocLengthUncompressed.toByteArray()); int read; StringBuffer tocFile = new StringBuffer(); while ((read = inflate.read(buffer, 0, length.intValue() > BUFFER_SIZE?BUFFER_SIZE:length.intValue())) > 0) { tocFile.append(new String(buffer, 0, read)); length = length.subtract(BigInteger.valueOf(read)); } return tocFile.toString(); } finally { inflate.close(); } } @Override public String toString() { return data; } public Map<String, XarEntry> getEntries() { return XarEntry.getEntries(doc); } } public class XarInputStream extends InputStream { private final InputStream _delegate; private final XarEntry _entry; private final MessageDigest _digest; public XarInputStream(XarEntry entry, InputStream input) { _entry = entry; _delegate = input; if (_entry.hasChecksum()) { _digest = _entry.getMessageDigest(getCksumName()); _digest.reset(); } else { _digest = null; } } @Override public int read() throws IOException { int result = _delegate.read(); if (result == -1) { if (!validChecksum()) { throw new XarException("invalid checksum"); } return result; } if (_digest != null) { _digest.update((byte)(result & BYTE_MASK)); } return result; } @Override public int read(byte[] buffer, int off, int len) throws IOException { int result = _delegate.read(buffer, off, len); if (result == -1) { if (!validChecksum()) { throw new XarException("invalid checksum"); } return result; } if (_digest != null) { _digest.update(buffer, off, result); } return result; } private boolean validChecksum() { if (_digest != null) { String checksum = toChecksum(_digest.digest()); if (_entry.hasChecksum() && !checksum.equals(_entry.getExtractedChecksum())) { return false; } } return true; } private String toChecksum(byte[] data) { StringBuffer checksum = new StringBuffer(); for (int j = 0; j < data.length; j++) { String hexString = Integer.toHexString(data[j] & BYTE_MASK); if (hexString.length() == 1) { checksum.append("0"); } checksum.append(hexString); } return checksum.toString(); } } // public static void main(String[] args) throws IOException, NoSuchAlgorithmException { // } public XarFile(String name) throws IOException { this(new File(name)); } public XarFile(File file) throws IOException { if (!file.exists() || file.length() < XAR_HEADER_SIZE) { throw new IOException("error reading header"); } this.file = file; setInputStream(new BufferedInputStream(new FileInputStream(file))); } public XarFile(InputStream stream) throws IOException { setInputStream(stream); } private void setInputStream(InputStream stream) throws IOException { inputStream = stream; header = new XarHeader(); if (header.magic != XAR_HEADER_MAGIC) { throw new XarException("invalid magic header"); } getToc(); try { run(); } catch (Exception e) { // TODO: handle exception } entries.putAll(getToc().getEntries()); } private XarToc getToc() { if (toc == null) { try { toc = new XarToc(); } catch (IOException e) { e.printStackTrace(); } } return toc; } public void run() throws IOException, NoSuchAlgorithmException { // System.out.println(toc); // XarEntry entry = getEntry("PackageInfo"); // InputStream in = getInputStream(entry); // Writer writer = new StringWriter(); // int i; // while ((i = in.read()) >= 0) { // writer.write(i); // } // System.out.print(writer.toString()); } public XarEntry getEntry(String name) { if (name == null) { throw new IllegalArgumentException("name"); } return entries.get(name); } public Map<String, XarEntry> getEntries() { return getToc().getEntries(); } public InputStream getInputStream(String name) throws IOException { return getInputStream(getEntry(name)); } public InputStream getInputStream(XarEntry entry) throws IOException { if (entry == null) { throw new IllegalArgumentException("entry"); } synchronized (this) { try { String compression = entry.getCompression(); long newOffset = 0; if (lastInputStream != null) { while(lastInputStream.read() != -1) { /* read to end of stream */ }; } if (entry.getOffset() <= currentOffset) { if (file == null) { throw new XarException("Cannot seek backwards through stream"); } lastInputStream.close(); inputStream = new BufferedInputStream(new FileInputStream(file)); long toSkip = header.size + header.tocLengthCompressed.longValue(); skipFully(inputStream, toSkip); currentOffset = 0; } newOffset = entry.getOffset() - currentOffset; currentOffset = entry.getOffset() + entry.getLength(); InputStream input = new BoundedInputStream(inputStream, newOffset, entry.getLength()); if (compression == null) { // Do nothing } else if ("bzip2".equals(compression)) { skipFully(input, 2); input = new CBZip2InputStream(input); } else if ("gzip".equals(compression)) { input = new GZIPInputStream(input); } lastInputStream = new XarInputStream(entry, input); return lastInputStream; } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } throw new XarException("something unexpected happened"); } } private static void skipFully(InputStream inputStream, long skip) throws IOException { long toSkip = skip; while (toSkip > 0) { toSkip -= inputStream.skip(toSkip); } } private static void readFully(InputStream inputStream, byte[] buffer) throws IOException { int read = 0; while (read < buffer.length) { read += inputStream.read(buffer, read, buffer.length - read); } } private String getCksumName() { if (header.checksumAlgorithm < 0 || header.checksumAlgorithm > XAR_CKSUM.length - 1) { return "unknown"; } return XAR_CKSUM[(int)header.checksumAlgorithm]; } private int readUint16() throws IOException { readFully(inputStream, byte2); return ((byte2[0] & BYTE_MASK) << Byte.SIZE) | byte2[1] & BYTE_MASK; } private long readUint32() throws IOException { readFully(inputStream, byte4); long result = 0; for (int i = 0; i < byte4.length; i++) { result |= (byte4[i] & BYTE_MASK) << (byte4.length - (i+1)) * Byte.SIZE; } return result; } private byte[] readByte8() throws IOException { readFully(inputStream, byte8); return byte8.clone(); } private BigInteger readUint64() throws IOException { return new BigInteger(readByte8()); } }