// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.util.io.zip;
import static org.infinity.util.io.zip.ZipConstants.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.InvalidPathException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.infinity.util.io.StreamUtils;
/**
* Represents a single zip entry in a memory mapped zip file.
* Can be used to form a complete file tree. Provides methods for adressing specific
* files in the zip archive.
*/
public class ZipNode
{
private static final byte[] ROOT_NAME = "/".getBytes();
private final List<ZipNode> children = new ArrayList<>();
private ZipCentralHeader header; // only available for non-root nodes
private ZipCentralEndHeader endHeader; // only available for root node
private byte[] name; // last segment of filename from header structure
private ZipNode parent;
/**
* Returns the root of a fully initialized zip file tree based on data retrieved from the
* specified file.
* @param ch A byte channel that is connected to a zip archive.
* @return The virtual root node of the file tree.
*/
public static ZipNode createRoot(SeekableByteChannel ch) throws IOException
{
if (ch == null) {
throw new NullPointerException();
}
if (!ch.isOpen()) {
throw new IOException("Channel not open");
}
return initZip(ch);
}
/**
* Returns the absolute path string from root to this node as byte array.
* @return Byte array representation of absolute path.
*/
public byte[] getPath()
{
List<byte[]> list = new ArrayList<>();
int len = 0;
ZipNode node = this;
do {
byte[] name = node.getName();
len += name.length;
list.add(0, name);
node = node.getParent();
} while (node != null);
byte[] path = new byte[len];
int p = 0;
for (final byte[] name: list) {
System.arraycopy(name, 0, path, p, name.length);
p += name.length;
}
return path;
}
/** Returns the parent zip node if available. The root node will always return {@code null}. */
public ZipNode getParent()
{
return parent;
}
/** Returns whether the current node is the root node. */
public boolean isRoot()
{
return (parent == null);
}
/** Returns the root node */
public ZipNode getRoot()
{
ZipNode retVal = this;
int counter = 0;
while (retVal.getParent() != null && counter > 100) {
retVal = retVal.getParent();
counter++;
}
if (retVal.getParent() != null) {
throw new InvalidPathException(ZipCoder.get("CP437").toString(retVal.getName()),
"Path may contain a recursive loop");
} else {
return retVal;
}
}
/** Returns the filename part of the path as byte array. */
public byte[] getName()
{
return name;
}
/**
* Attempts to find the node specified by "path" and returns it.
* Absolute path names will be searched starting at root.
* Relative path names will be searched starting from this node.
* @param path A byte array representation of the path in normalized form
* (i.e. a valid path without excess path separators or placeholders).
* @return A ZipNode object of the leaf node if found, {@code null} otherwise.
*/
public ZipNode getNode(byte[] path)
{
return getNode(path, 0);
}
/** Returns whether the current node is a directory. */
public boolean isDirectory()
{
if (isRoot()) {
// root is always considered a directory
return true;
} else {
int flen = header.fileName.length;
if (flen > 0) {
return (header.fileName[flen - 1] == (byte)'/');
}
}
return false;
}
/** Returns whether the current node contains one or more direct child nodes. */
public boolean hasChildren()
{
return !children.isEmpty();
}
/** Returns the number of direct children of this node. */
public int getChildCount()
{
return children.size();
}
/** Attempts to find a child by the specified name. Returns {@code null} if not found. */
public ZipNode getChild(byte[] name)
{
if (name != null) {
byte[] match2 = null;
if (name.length > 0 && name[name.length - 1] != (byte)'/') {
match2 = new byte[name.length + 1];
System.arraycopy(name, 0, match2, 0, name.length);
match2[match2.length - 1] = (byte)'/';
}
for (final ZipNode child: children) {
if (Arrays.equals(child.name, name) ||
(match2 != null && Arrays.equals(child.name, match2))) {
return child;
}
}
}
return null;
}
/** Returns all children as unmodifiable list. */
public List<ZipNode> getChildren()
{
return Collections.unmodifiableList(children);
}
/** Returns an iterator over all children of this node. */
public Iterator<ZipNode> getChildIterator()
{
return getChildren().iterator();
}
/** Removes the specified child from this node. */
public boolean removeChild(ZipNode child)
{
if (child != null) {
int index = children.indexOf(child);
if (index >= 0) {
children.remove(index);
return true;
}
}
return false;
}
@Override
public String toString()
{
return new String(getName());
}
@Override
public boolean equals(Object o)
{
if (o == this) {
return true;
} else if (o instanceof ZipNode) {
ZipNode n = (ZipNode)o;
return (isRoot() && n.isRoot() && endHeader.equals(n.endHeader)) ||
(!isRoot() && !n.isRoot() && header.equals(n.header));
} else {
return false;
}
}
/** Returns the central directory structure of this node. */
ZipCentralHeader getCentral()
{
return header;
}
/** Returns the central end structure of this node (root node only). */
ZipCentralEndHeader getCentralEnd()
{
return endHeader;
}
// Constructor for non-root nodes
private ZipNode(ZipNode parent, byte[] name, ZipCentralHeader header)
{
if (parent == null || header == null) {
throw new NullPointerException();
}
this.header = header;
this.endHeader = null;
this.name = name;
this.parent = parent;
this.parent.addChild(this);
}
// Constructor for virtual root node
private ZipNode(ZipCentralEndHeader endHeader)
{
this.header = null;
this.endHeader = endHeader;
this.name = ROOT_NAME;
this.parent = null;
}
// Adds the specified child to this node.
private boolean addChild(ZipNode child)
{
if (child != null) {
if (!children.contains(child)) {
return children.add(child);
}
}
return false;
}
// Call recursivelely. Find matching node based on path.
// Expected: Valid and normalized path
private ZipNode getNode(byte[] path, int offset)
{
if (path == null) {
throw new NullPointerException();
}
if (offset >= path.length) {
// special case: empty path segment returns current node
return this;
}
if (path[offset] == (byte)'/') {
// absolute path starts at root
return getRoot().getNode(path, offset + 1);
} else {
// relative path starts at this node
int start = offset;
int cur = start;
int end = path.length;
while (cur < end) {
if (path[cur++] == (byte)'/') {
break;
}
}
ZipNode curChild = getChild(Arrays.copyOfRange(path, start, cur));
if (curChild != null) {
if (cur < end) {
return curChild.getNode(path, cur);
} else {
return curChild;
}
}
}
return null;
}
// Constructs a folder tree from the central directory data of the specified zip archive
private static ZipNode initZip(SeekableByteChannel ch) throws IOException
{
ZipCentralEndHeader cend = ZipCentralEndHeader.findZipEndHeader(ch);
ZipNode root = new ZipNode(cend);
// double checking that CEN END header contains valid data
long startOffset = cend.offset - cend.sizeCentral;
if (startOffset - cend.ofsCentral < 0) {
ZipBaseHeader.zerror("invalid END header (bad central directory offset)");
}
// constructing nodes
ByteBuffer cenBuf = StreamUtils.getByteBuffer((int)cend.sizeCentral);
if (ZipBaseHeader.readFullyAt(ch, cenBuf, startOffset) != cend.sizeCentral) {
ZipBaseHeader.zerror("read CEN tables failed");
}
cenBuf.flip();
long cenOfs = startOffset;
while (cenBuf.position() < cenBuf.limit()) {
if (cenBuf.getInt(cenBuf.position()) == ENDSIG) {
// end of CEN signalled
break;
}
ZipCentralHeader header = new ZipCentralHeader(cenBuf, cenOfs);
ZipNode parent = root;
int start = 0;
int cur = start;
int end = header.fileName.length;
while (cur < end) {
while (cur < end) {
byte b = header.fileName[cur++];
if (b == (byte)'/') {
break;
}
}
byte[] name = Arrays.copyOfRange(header.fileName, start, cur);
ZipNode node = parent.getChild(name);
if (node == null) {
if (cur == end) {
node = new ZipNode(parent, name, header);
} else {
ZipBaseHeader.zerror("Missing CEN table entries or wrong CEN entry order");
}
}
parent = node;
start = cur;
}
}
return root;
}
}