ZipReaderIo.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.nio;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import net.sourceforge.plantuml.code.deflate.ByteBitInputStream;
import net.sourceforge.plantuml.code.deflate.Decompressor;
/**
* Reads a ZIP archive with java.io only, for the C build, where
* java.util.zip.ZipFile is not available. The archive is read whole in memory,
* its central directory gives the entries, and the deflated ones go through the
* pure Java inflater of PlantUML.
* <p>
* Neither encryption nor multi-volume archives are supported.
*/
final class ZipReaderIo {
private static final int METHOD_STORED = 0;
private static final int METHOD_DEFLATED = 8;
private static final class Entry {
private final int method;
private final long compressedSize;
private final long size;
private final long localHeaderOffset;
private Entry(int method, long compressedSize, long size, long localHeaderOffset) {
this.method = method;
this.compressedSize = compressedSize;
this.size = size;
this.localHeaderOffset = localHeaderOffset;
}
}
// The last archive read: the same one is asked again for each include
private static ZipReaderIo last;
private final String key;
private final byte[] data;
private final Map<String, Entry> entries = new HashMap<String, Entry>();
private ZipReaderIo(String key, byte[] data) {
this.key = key;
this.data = data;
}
/**
* The reader of this file, or null if it is not a readable ZIP archive.
*/
static ZipReaderIo open(File file) throws IOException {
if (file.isFile() == false || file.canRead() == false)
return null;
final String key = file.getAbsolutePath() + "|" + file.length() + "|" + file.lastModified();
if (last != null && last.key.equals(key))
return last;
final ZipReaderIo result = new ZipReaderIo(key, readAll(file));
if (result.readCentralDirectory() == false)
return null;
last = result;
return result;
}
private static byte[] readAll(File file) throws IOException {
final InputStream is = new FileInputStream(file);
try {
final byte[] result = new byte[(int) file.length()];
int done = 0;
while (done < result.length) {
final int n = is.read(result, done, result.length - done);
if (n < 0)
break;
done += n;
}
if (done < result.length) {
final byte[] shorter = new byte[done];
System.arraycopy(result, 0, shorter, 0, done);
return shorter;
}
return result;
} finally {
is.close();
}
}
private int u16(int pos) {
return (data[pos] & 0xFF) | ((data[pos + 1] & 0xFF) << 8);
}
private long u32(int pos) {
return (u16(pos) & 0xFFFFL) | ((u16(pos + 2) & 0xFFFFL) << 16);
}
private boolean readCentralDirectory() {
// End of central directory record: 22 bytes, then an optional comment
int end = -1;
for (int pos = data.length - 22; pos >= 0 && pos >= data.length - 22 - 0xFFFF; pos--)
if (u32(pos) == 0x06054b50L) {
end = pos;
break;
}
if (end == -1)
return false;
final int count = u16(end + 10);
long pos = u32(end + 16);
for (int i = 0; i < count; i++) {
if (pos < 0 || pos + 46 > data.length || u32((int) pos) != 0x02014b50L)
return false;
final int p = (int) pos;
final int method = u16(p + 10);
final long compressedSize = u32(p + 20);
final long size = u32(p + 24);
final int nameLength = u16(p + 28);
final int extraLength = u16(p + 30);
final int commentLength = u16(p + 32);
final long localHeaderOffset = u32(p + 42);
if (p + 46 + nameLength > data.length)
return false;
final String name = new String(data, p + 46, nameLength, StandardCharsets.UTF_8);
entries.put(name, new Entry(method, compressedSize, size, localHeaderOffset));
pos = p + 46L + nameLength + extraLength + commentLength;
}
return true;
}
/**
* Whether this is a file (not a directory) of the archive.
*/
boolean contains(String name) {
return entries.containsKey(name) && name.endsWith("/") == false;
}
/**
* The uncompressed content of this entry, or null if there is none.
*/
byte[] read(String name) throws IOException {
final Entry entry = entries.get(name);
if (entry == null || name.endsWith("/"))
return null;
final long local = entry.localHeaderOffset;
if (local < 0 || local + 30 > data.length || u32((int) local) != 0x04034b50L)
throw new IOException("Bad ZIP entry: " + name);
final int start = (int) local + 30 + u16((int) local + 26) + u16((int) local + 28);
if (entry.compressedSize < 0 || start + entry.compressedSize > data.length)
throw new IOException("Bad ZIP entry: " + name);
final int length = (int) entry.compressedSize;
if (entry.method == METHOD_STORED) {
final byte[] result = new byte[length];
System.arraycopy(data, start, result, 0, length);
return result;
}
if (entry.method != METHOD_DEFLATED)
throw new IOException("Unsupported ZIP compression method " + entry.method + ": " + name);
// The bit reader looks a little beyond the last block: pad as CompressionZlib does
final byte[] padded = new byte[length + 256];
System.arraycopy(data, start, padded, 0, length);
try {
final byte[] result = Decompressor
.decompress(new ByteBitInputStream(new ByteArrayInputStream(padded)));
if (result.length != entry.size)
throw new IOException("Bad ZIP entry size: " + name);
return result;
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException("Bad ZIP entry: " + name + " (" + e + ")");
}
}
}