/*-
* 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="http://www.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);
}
}