Catalog.java
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.thumbsdb;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.ole2.CompoundDocument;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.IIOException;
import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Iterator;
/**
* Represents a {@code Catalog} structure, typically found in a {@link com.twelvemonkeys.io.ole2.CompoundDocument}.
*
* @see <a href="http://www.petedavis.net/MySite/DynPageView.aspx?pageid=31">PeteDavis.NET</a>
*
* @author <a href="mailto:harald.kuhr@gmail.no">Harald Kuhr</a>
* @author last modified by $Author: haku$
* @version $Id: Catalog.java,v 1.0 01.feb.2007 17:19:59 haku Exp$
*/
// TODO: Consider moving this one to io.ole2
public final class Catalog implements Iterable<Catalog.CatalogItem> {
private final CatalogHeader header;
private final CatalogItem[] items;
Catalog(final CatalogHeader pHeader, final CatalogItem[] pItems) {
header = pHeader;
items = pItems;
}
/**
* Reads the {@code Catalog} entry from the given input stream.
*
* @param pInput the input stream
* @return a new {@code Catalog}
*
* @throws java.io.IOException if an I/O exception occurs during read
*/
public static Catalog read(final InputStream pInput) throws IOException {
DataInput dataInput = new LittleEndianDataInputStream(pInput);
return read(dataInput);
}
/**
* Reads the {@code Catalog} entry from the given input stream.
* <p>
* The data is assumed to be in little endian byte order.
* </p>
*
* @param pDataInput the input stream
* @return a new {@code Catalog}
*
* @throws java.io.IOException if an I/O exception occurs during read
*/
public static Catalog read(final DataInput pDataInput) throws IOException {
CatalogHeader header = CatalogHeader.read(pDataInput);
CatalogItem[] items = new CatalogItem[header.getThumbnailCount()];
for (int i = 0; i < header.getThumbnailCount(); i++) {
CatalogItem item = CatalogItem.read(pDataInput);
//System.out.println("item: " + item);
int index = item.getItemId() - 1;
if (index < 0 || index >= items.length) {
throw new IIOException("Thumbs.db catalog item id out of range: " + item.getItemId());
}
items[index] = item;
}
return new Catalog(header, items);
}
public final int getThumbnailCount() {
return header.thumbCount;
}
public final int getMaxThumbnailWidth() {
return header.thumbWidth;
}
public final int getMaxThumbnailHeight() {
return header.thumbHeight;
}
final CatalogItem getItem(final int pIndex) {
return items[pIndex];
}
final CatalogItem getItem(final String pName) {
return items[getIndex(pName)];
}
final int getItemId(final int pIndex) {
return items[pIndex].getItemId();
}
public final int getIndex(final String pName) {
for (int i = 0; i < items.length; i++) {
CatalogItem item = items[i];
if (item.getName().equals(pName)) {
return i;
}
}
return -1;
}
public final String getStreamName(final int pIndex) {
return StringUtil.reverse(String.valueOf(getItemId(pIndex)));
}
public final String getName(String pStreamName) {
return getName(Integer.parseInt(StringUtil.reverse(pStreamName)));
}
final String getName(int pItemId) {
return items[pItemId - 1].getName();
}
@Override
public String toString() {
return String.format("%s[%s]", getClass().getSimpleName(), header);
}
public Iterator<CatalogItem> iterator() {
return new Iterator<CatalogItem>() {
int currentIdx;
public boolean hasNext() {
return currentIdx < items.length;
}
public CatalogItem next() {
return items[currentIdx++];
}
public void remove() {
throw new UnsupportedOperationException("Remove not supported");
}
};
}
private static class CatalogHeader {
short reserved1;
short reserved2;
int thumbCount;
int thumbWidth;
int thumbHeight;
CatalogHeader() {
}
public static CatalogHeader read(final DataInput pDataInput) throws IOException {
CatalogHeader header = new CatalogHeader();
header.reserved1 = pDataInput.readShort(); // length?
header.reserved2 = pDataInput.readShort(); // version? flags?
header.thumbCount = pDataInput.readInt();
header.thumbWidth = pDataInput.readInt();
header.thumbHeight = pDataInput.readInt();
return header;
}
public int getThumbnailCount() {
return thumbCount;
}
public int getThumbHeight() {
return thumbHeight;
}
public int getThumbWidth() {
return thumbWidth;
}
@Override
public String toString() {
return String.format(
"%s: %s %s thumbs: %d maxWidth: %d maxHeight: %d",
getClass().getSimpleName(), reserved1, reserved2, thumbCount, thumbWidth, thumbHeight
);
}
}
public static final class CatalogItem {
int size;
int itemId; // Reversed stream name
private long lastModified;
String filename;
private static CatalogItem read(final DataInput pDataInput) throws IOException {
CatalogItem item = new CatalogItem();
// Size must be minimum 16 bytes, path name can be at most 260 UTF16LE chars, must be even
item.size = pDataInput.readInt();
if (item.size <= 16 || item.size - 16 > 520 || item.size % 2 != 0) {
throw new IIOException("Illegal Thumbs.db Catalog item size: " + item.size);
}
item.itemId = pDataInput.readInt();
item.lastModified = CompoundDocument.toJavaTimeInMillis(pDataInput.readLong());
// The remainder holds the NUL-terminated UTF-16LE file name plus trailing padding
int nameSize = (item.size - 16) / 2;
char[] name = new char[nameSize];
char ch;
int nameLen = 0;
while ((ch = pDataInput.readChar()) != 0) {
name[nameLen++] = ch;
}
pDataInput.skipBytes(item.size - 16 - (nameLen + 1) * 2);
item.filename = StringUtil.getLastElement(new String(name, 0, nameLen), "\\");
return item;
}
public String getName() {
return filename;
}
public int getItemId() {
return itemId;
}
public long lastModified() {
return lastModified;
}
@Override
public String toString() {
return String.format(
"%s: size: %d itemId: %d lastModified: %s fileName: %s",
getClass().getSimpleName(), size, itemId, new Date(lastModified), filename
);
}
}
}