/*-
 * Copyright (C) 2014 Erik Larsson
 *
 * This program 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.
 *
 * This program is 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 program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.catacombae.hfsexplorer.tools;

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import org.catacombae.hfs.AttributesFile;
import org.catacombae.hfs.types.decmpfs.DecmpfsHeader;
import org.catacombae.hfs.types.hfscommon.CommonBTIndexRecord;
import org.catacombae.hfs.types.hfscommon.CommonBTNode;
import org.catacombae.hfs.types.hfscommon.CommonHFSAttributesIndexNode;
import org.catacombae.hfs.types.hfscommon.CommonHFSAttributesKey;
import org.catacombae.hfs.types.hfscommon.CommonHFSAttributesLeafNode;
import org.catacombae.hfs.types.hfscommon.CommonHFSAttributesLeafRecord;
import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogLeafRecord;
import org.catacombae.hfs.types.hfsplus.HFSPlusAttributesData;
import org.catacombae.hfs.types.hfsplus.HFSPlusAttributesLeafRecordData;
import org.catacombae.io.ReadableFileStream;
import org.catacombae.io.ReadableRandomAccessStream;
import org.catacombae.storage.fs.FileSystemHandler;
import org.catacombae.storage.fs.FileSystemHandlerFactory;
import org.catacombae.storage.fs.FileSystemMajorType;
import org.catacombae.storage.fs.hfscommon.HFSCommonFileSystemRecognizer;
import org.catacombae.storage.fs.hfsplus.HFSPlusFileSystemHandler;
import org.catacombae.storage.io.ReadableStreamDataLocator;
import org.catacombae.storage.io.win32.ReadableWin32FileStream;
import org.catacombae.util.Util;

/**
 * @author <a href="https://catacombae.org" target="_top">Erik Larsson</a>
 */
public class ScanDecmpfs {
    public static void main(String[] args) {
        final boolean verbose;
        final String fsPath;

        if(args.length == 2 && args[0].equals("-v")) {
            verbose = true;
            fsPath = args[1];
        }
        else if(args.length == 1) {
            verbose = false;
            fsPath = args[0];
        }
        else {
            System.err.println("usage: ScanDecmpfs [-v] <device|file>");
            System.err.println();
            System.err.println("    Scans an HFS+/HFSX volume for decmpfs " +
                    "compressed files and prints the");
            System.err.println("    file's CNID, compression type and " +
                    "decompressed size.");
            System.err.println("    If '-v' is supplied, the path of the " +
                    "compressed file is also resolved and");
            System.err.println("    printed after the decompressed size.");
            System.exit(1);
            return;
        }

        final ReadableRandomAccessStream fsStream =
                ReadableWin32FileStream.isSystemSupported() ?
                    new ReadableWin32FileStream(fsPath) :
                    new ReadableFileStream(fsPath);

        final FileSystemHandlerFactory fsHandlerFactory;
        switch(HFSCommonFileSystemRecognizer.detectFileSystem(fsStream, 0)) {
            case HFS_WRAPPED_HFS_PLUS:
            case HFS_PLUS:
                fsHandlerFactory =
                        FileSystemMajorType.APPLE_HFS_PLUS.
                        createDefaultHandlerFactory();
                break;
            case HFSX:
                fsHandlerFactory =
                        FileSystemMajorType.APPLE_HFSX.
                        createDefaultHandlerFactory();
                break;
            default:
                System.err.println("No HFS+/HFSX filesystem detected.");
                System.exit(1);
                return;
        }

        final FileSystemHandler fsHandlerGeneric =
                fsHandlerFactory.createHandler(
                new ReadableStreamDataLocator(fsStream));
        if(!(fsHandlerGeneric instanceof HFSPlusFileSystemHandler)) {
            System.err.println("Unexpected: File system handler object is " +
                    "not of HFSPlusFileSystemHandler class (class: " +
                    fsHandlerGeneric.getClass() + ").");
            System.exit(1);
            return;
        }

        final HFSPlusFileSystemHandler fsHandler =
                (HFSPlusFileSystemHandler) fsHandlerGeneric;
        final AttributesFile attributesFile =
                fsHandler.getFSView().getAttributesFile();

        LinkedList<Long> nodeQueue = new LinkedList<Long>();
        nodeQueue.addLast(attributesFile.getRootNodeNumber());

        /* Depth-first search for "com.apple.decmpfs" attribute records. */
        while(!nodeQueue.isEmpty()) {
            long curNodeNumber = nodeQueue.removeFirst();
            CommonBTNode curNode = attributesFile.getNode(curNodeNumber);

            if(curNode instanceof CommonHFSAttributesIndexNode) {
                CommonHFSAttributesIndexNode indexNode =
                        (CommonHFSAttributesIndexNode) curNode;

                List<CommonBTIndexRecord<CommonHFSAttributesKey>> records =
                        indexNode.getBTKeyedRecords();
                ListIterator<CommonBTIndexRecord<CommonHFSAttributesKey>> it =
                        records.listIterator(records.size());

                /* For the search to be depth first, add elements in reverse
                 * order. */
                while(it.hasPrevious()) {
                    nodeQueue.addFirst(it.previous().getIndex());
                }
            }
            else if(curNode instanceof CommonHFSAttributesLeafNode) {
                final CommonHFSAttributesLeafNode leafNode =
                        (CommonHFSAttributesLeafNode) curNode;
                for(CommonHFSAttributesLeafRecord rec :
                        leafNode.getLeafRecords())
                {
                    final CommonHFSAttributesKey k = rec.getKey();
                    if(!new String(k.getAttrName(), 0, k.getAttrNameLen()).
                            equals("com.apple.decmpfs"))
                    {
                        continue;
                    }
                    else if(k.getStartBlock() != 0) {
                        System.err.println("[WARNING] " +
                                k.getFileID().toLong() + " has " +
                                "com.apple.decmpfs attribute with non-0 " +
                                "start block (" + k.getStartBlock() + "). " +
                                "Skipping...");
                        continue;
                    }

                    final HFSPlusAttributesLeafRecordData data =
                            rec.getRecordData();
                    if(!(data instanceof HFSPlusAttributesData)) {
                        System.err.println("[WARNING] " +
                                k.getFileID().toLong() + " has " +
                                "com.apple.decmpfs attribute without inline " +
                                "data (" + data.getRecordTypeAsString() +
                                "). Skipping...");
                        continue;
                    }

                    final DecmpfsHeader header =
                            new DecmpfsHeader(
                            ((HFSPlusAttributesData) data).getAttrData(), 0);
                    if(header.getMagic() != DecmpfsHeader.MAGIC) {
                        System.err.println("[WARNING] " +
                                k.getFileID().toLong() + " has " +
                                "com.apple.decmpfs attribute with " +
                                "mismatching magic (expected: 0x" +
                                Util.toHexStringBE((int) DecmpfsHeader.MAGIC) +
                                ", actual: 0x" +
                                Util.toHexStringBE(header.getRawMagic()) +
                                "). Skipping...");
                        continue;
                    }

                    final StringBuilder pathBuilder;
                    if(verbose) {
                        pathBuilder = new StringBuilder();

                        boolean firstComponent = true;
                        for(CommonHFSCatalogLeafRecord pathComponent :
                                fsHandler.getFSView().getCatalogFile().
                                getPathTo(k.getFileID()))
                        {
                            /* Skip name of root directory. */
                            if(!firstComponent) {
                                final char[] nodeName =
                                        fsHandler.getFSView().decodeString(
                                        pathComponent.getKey().getNodeName()).
                                        toCharArray();

                                for(int i = 0; i < nodeName.length; ++i) {
                                    /* '/' transformed into ':' and vice versa.
                                     * This is part of the POSIX-translation of
                                     * filenames in HFS+ (original Mac OS had
                                     * ':' as a reserved character, while '/' is
                                     * reserved in Mac OS X/POSIX). */
                                    if(nodeName[i] == '/') {
                                        nodeName[i] = ':';
                                    }
                                    else if(nodeName[i] == ':') {
                                        /* Note: This should really be
                                         *       considered an illegal HFS+
                                         *       character. */
                                        nodeName[i] = '/';
                                    }
                                }

                                pathBuilder.append('/').append(nodeName);
                            }
                            else {
                                firstComponent = false;
                            }
                        }
                    }
                    else {
                        pathBuilder = null;
                    }

                    System.out.println("CNID: " + k.getFileID().toLong() + " " +
                            "Type: " + header.getCompressionType() + " " +
                            "Size: " + header.getFileSize() +
                            (pathBuilder != null ? " Path: " +
                            pathBuilder.toString() : ""));
                }
            }
            else {
                System.err.println("[WARNING] Unexpected attributes B-tree " +
                        "node type: " + curNode.getClass());
            }
        }

        fsHandler.close();
        System.exit(0);
    }
}
