/******************************************************************************* * Copyright (c) 2015 IBH SYSTEMS GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation *******************************************************************************/ package org.eclipse.packagedrone.utils.rpm; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tukaani.xz.LZMAInputStream; import org.tukaani.xz.XZInputStream; import com.google.common.io.CountingInputStream; public class RpmInputStream extends InputStream { private final static Logger logger = LoggerFactory.getLogger ( RpmInputStream.class ); private static final byte[] LEAD_MAGIC = new byte[] { (byte)0xED, (byte)0xAB, (byte)0xEE, (byte)0xDB }; private static final byte[] HEADER_MAGIC = new byte[] { (byte)0x8E, (byte)0xAD, (byte)0xE8 }; private static final byte[] DUMMY = new byte[128]; private final DataInputStream in; private boolean closed; private RpmLead lead; private RpmHeader<RpmSignatureTag> signatureHeader; private RpmHeader<RpmTag> payloadHeader; private InputStream payloadStream; private CpioArchiveInputStream cpioStream; private final CountingInputStream count; public RpmInputStream ( final InputStream in ) { this.count = new CountingInputStream ( in ); this.in = new DataInputStream ( this.count ); } @Override public void close () throws IOException { if ( !this.closed ) { this.in.close (); this.closed = true; } } protected void ensureInit () throws IOException { if ( this.lead == null ) { this.lead = readLead (); } if ( this.signatureHeader == null ) { this.signatureHeader = readHeader ( true ); } if ( this.payloadHeader == null ) { this.payloadHeader = readHeader ( false ); } // set up content stream if ( this.payloadStream == null ) { this.payloadStream = setupPayloadStream (); this.cpioStream = new CpioArchiveInputStream ( this.payloadStream ); // we did ensure that we only support CPIO before } } private InputStream setupPayloadStream () throws IOException { final Object payloadFormatValue = this.payloadHeader.getRawTags ().get ( RpmTag.PAYLOAD_FORMAT.getValue () ); final Object payloadCodingValue = this.payloadHeader.getRawTags ().get ( RpmTag.PAYLOAD_CODING.getValue () ); if ( payloadFormatValue != null && ! ( payloadFormatValue instanceof String ) ) { throw new IOException ( "Payload format must be a single string" ); } if ( payloadFormatValue != null && ! ( payloadCodingValue instanceof String ) ) { throw new IOException ( "Payload coding must be a single string" ); } String payloadFormat = (String)payloadFormatValue; String payloadCoding = (String)payloadCodingValue; if ( payloadFormat == null || payloadFormat.isEmpty () ) { payloadFormat = "cpio"; } if ( payloadCoding == null || payloadCoding.isEmpty () ) { payloadCoding = "gzip"; } if ( !"cpio".equals ( payloadFormat ) ) { throw new IOException ( String.format ( "Unknown payload format: %s", payloadFormat ) ); } switch ( payloadCoding ) { case "none": return this.in; case "gzip": return new GzipCompressorInputStream ( this.in ); case "bzip2": return new BZip2CompressorInputStream ( this.in ); case "lzma": return new LZMAInputStream ( this.in ); case "xz": return new XZInputStream ( this.in ); default: throw new IOException ( String.format ( "Unknown coding: %s", payloadCoding ) ); } } public CpioArchiveInputStream getCpioStream () { return this.cpioStream; } public RpmLead getLead () throws IOException { ensureInit (); return this.lead; } public RpmHeader<RpmSignatureTag> getSignatureHeader () throws IOException { ensureInit (); return this.signatureHeader; } public RpmHeader<RpmTag> getPayloadHeader () throws IOException { ensureInit (); return this.payloadHeader; } protected RpmLead readLead () throws IOException { final byte[] magic = readComplete ( 4 ); if ( !Arrays.equals ( magic, LEAD_MAGIC ) ) { throw new IOException ( String.format ( "File corrupt: Expected magic %s, read: %s", Arrays.toString ( LEAD_MAGIC ), Arrays.toString ( magic ) ) ); } final byte[] version = readComplete ( 2 ); skipFully ( 4 ); // TYPE + ARCH final byte[] nameData = readComplete ( 66 ); // NAME final String name = StandardCharsets.UTF_8.decode ( ByteBuffer.wrap ( nameData ) ).toString (); skipFully ( 2 ); // OS final int sigType = this.in.readUnsignedShort (); skipFully ( 16 ); // RESERVED return new RpmLead ( version[0], version[1], name, sigType ); } protected <T extends RpmBaseTag> RpmHeader<T> readHeader ( final boolean withPadding ) throws IOException { final long start = this.count.getCount (); final byte[] magic = readComplete ( 3 ); if ( !Arrays.equals ( magic, HEADER_MAGIC ) ) { throw new IOException ( String.format ( "File corrupt: Expected entry magic %s, read: %s", Arrays.toString ( HEADER_MAGIC ), Arrays.toString ( magic ) ) ); } final byte version = this.in.readByte (); if ( version != 1 ) { throw new IOException ( String.format ( "File corrupt: Invalid header entry version: %s (valid: 1)", version ) ); } skipFully ( 4 ); // RESERVED final int indexCount = this.in.readInt (); final long storeSize = this.in.readInt () & 0xFFFFFFFF; final RpmEntry[] entries = new RpmEntry[indexCount]; for ( int i = 0; i < indexCount; i++ ) { entries[i] = readEntry (); } final ByteBuffer store = ByteBuffer.wrap ( readComplete ( (int)storeSize ) ); // FIXME: bad casting ... for ( int i = 0; i < indexCount; i++ ) { entries[i].fillFromStore ( store ); } if ( withPadding ) { // pad remaining bytes - to 8 final long rem = storeSize % 8; if ( rem > 0 ) { final int skip = (int) ( 8 - rem ); logger.debug ( "Skipping {} pad bytes", skip ); skipFully ( skip ); } } final long end = this.count.getCount (); return new RpmHeader<T> ( entries, start, end - start ); } private RpmEntry readEntry () throws IOException { final int tag = this.in.readInt (); final int type = this.in.readInt (); final int offset = this.in.readInt (); final int count = this.in.readInt (); return new RpmEntry ( tag, type, offset, count ); } private byte[] readComplete ( final int size ) throws IOException { final byte[] result = new byte[size]; this.in.readFully ( result ); return result; } private void skipFully ( final int count ) throws IOException { this.in.readFully ( DUMMY, 0, count ); } // forward methods @Override public void reset () throws IOException { ensureInit (); this.payloadStream.reset (); } @Override public int read () throws IOException { ensureInit (); return this.payloadStream.read (); } @Override public long skip ( final long n ) throws IOException { ensureInit (); return this.payloadStream.skip ( n ); } @Override public int available () throws IOException { ensureInit (); return this.payloadStream.available (); } @Override public int read ( final byte[] b ) throws IOException { ensureInit (); return this.payloadStream.read ( b ); } @Override public int read ( final byte[] b, final int off, final int len ) throws IOException { return this.payloadStream.read ( b, off, len ); } }