ZipArchiveInputStream.java
package xyz.apollosoftware.bibliothiki.compression.formats.zip;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import xyz.apollosoftware.bibliothiki.compression.ArchiveEntry;
import xyz.apollosoftware.bibliothiki.compression.ArchiveInputStream;
import xyz.apollosoftware.bibliothiki.compression.CompressionException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static xyz.apollosoftware.bibliothiki.compression.utils.Streams.pipeNBytes;
/**
* An adapter for the built-in {@code java.util.zip} capability.
*/
public class ZipArchiveInputStream extends ArchiveInputStream {
/**
* Peek the first four bytes of the stream to see if they match the ZIP
* magic number.
*
* @param stream The stream to check.
* @return True, if the stream is a ZIP stream, otherwise false.
*/
public static boolean isZipStream(@NonNull final InputStream stream) {
if (!stream.markSupported()) {
throw new UnsupportedOperationException("Unable to peek stream bytes (mark not supported)");
}
boolean isZip = false;
stream.mark(4);
try {
final var magic = stream.readNBytes(4);
if (magic[0] == (byte) 0x50 &&
magic[1] == (byte) 0x4B &&
magic[2] == (byte) 0x03 &&
magic[3] == (byte) 0x04) {
isZip = true;
}
} catch (IOException ignored) {
}
try {
stream.reset();
} catch (IOException ex) {
throw new CompressionException("Failed to rewind stream header", ex);
}
return isZip;
}
@NonNull
private final ZipInputStream zipStream;
@Nullable
private ZipEntry zipEntry = null;
/**
* Construct a {@link ZipArchiveInputStream} that encapsulates the given
* {@link InputStream}.
*
* @param stream The input stream that will provide ZIP data.
*/
public ZipArchiveInputStream(@NonNull final InputStream stream) {
super(stream);
this.zipStream = new ZipInputStream(stream);
}
@Override
@Nullable
public ArchiveEntry getNextEntry() {
try {
zipEntry = zipStream.getNextEntry();
} catch (IOException ex) {
throw new CompressionException("I/O error whilst getting next ZIP entry", ex);
}
return Optional.ofNullable(zipEntry)
.map(entry -> ArchiveEntry.builder()
.name(entry.getName())
.type(entry.isDirectory() ? ArchiveEntry.Type.DIRECTORY : ArchiveEntry.Type.FILE)
// (permission data is not available for a ZIP)
.size(entry.getSize())
.build()
)
.orElse(null);
}
@Override
public void writeCurrentEntryTo(@NonNull OutputStream stream) {
if (zipEntry == null) {
throw new CompressionException("Cannot write current entry as there is no current entry");
}
try {
pipeNBytes(this.zipStream, stream, zipEntry.getSize());
} catch (CompressionException ex) {
close();
throw ex;
}
}
@Override
public void closeCurrentEntry() {
if (zipEntry != null) {
try {
zipStream.closeEntry();
zipEntry = null;
} catch (IOException ex) {
throw new CompressionException("Failed to close ZIP entry", ex);
}
}
}
@Override
public void close() {
try {
zipStream.close();
super.close();
} catch (final IOException ex) {
throw new CompressionException("Failed to close ZIP stream", ex);
}
}
}