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
            );
        }
    }
}