/*-
* Copyright (C) 2008 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.fs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.catacombae.hfsexplorer.types.applesingle.AppleSingleHeader;
import org.catacombae.hfsexplorer.types.applesingle.AttributeEntry;
import org.catacombae.hfsexplorer.types.applesingle.AttributeHeader;
import org.catacombae.hfsexplorer.types.applesingle.EntryDescriptor;
import org.catacombae.util.Util;
import org.catacombae.util.Util.Pair;
/**
* @author <a href="http://www.catacombae.org/" target="_top">Erik Larsson</a>
*/
public class AppleSingleBuilder {
/*
* Layout of an AppleSingle file:
* -------------------------------|
* | AppleSingleHeader (26 bytes) |
* |------------------------------|
* | EntryDescriptor[0] (12 bytes)|
* --------------------------------
* : : Number of entries is determined by the numEntries field in
* -------------------------------| the header.
* | EntryDescriptor[n] |
* |------------------------------|
* : : <- the format allows for padding
* |------------------------------| <- pos, len determined by EntryDescriptor[0]
* | entry[0] |
* : :
* -------------------------------| <- pos, len determined by EntryDescriptor[n]
* | entry[n] |
* : :
* -------------------------------|
*/
/**
* Type enumerating all valid file types for an AppleSingle file.
*/
public static enum FileType {
/**
* Indicates an AppleSingle file, containing both a data fork and a resource fork (and
* optionally other attributes).
*/
APPLESINGLE(0x00051600),
/**
* Indicates an AppleDouble file, containing only a resource fork (and optionally other
* attributes). The same file format (AppleSingle) is used, but the data fork is omitted.
*/
APPLEDOUBLE(0x00051607);
private final int magic;
private FileType(int magic) {
this.magic = magic;
}
/**
* Returns the magic number associated with this file type.
* @return the magic number associated with this file type.
*/
public int getMagic() { return magic; }
}
public static enum AppleSingleVersion {
/** The AppleSingle format used in A/UX and possibly Mac OS Classic / early Mac OS X versions. */
VERSION_1_0(0x00010000),
/** The version used in Mac OS X Leopard and possibly earlier Mac OS X versions. */
VERSION_2_0(0x00020000);
private final int versionNumber;
private AppleSingleVersion(int versionNumber) {
this.versionNumber = versionNumber;
}
public int getVersionNumber() {
return versionNumber;
}
}
public static enum FileSystem {
MACOS("Macintosh"),
MACOS_X("Mac OS X"),
PRODOS("ProDOS"),
MS_DOS("MS-DOS"),
UNIS("Unix"),
VMS("VAX VMS");
private final String identifier;
private FileSystem(String identifier) {
this.identifier = identifier;
}
public byte[] getIdentifier() {
char[] chars = identifier.toCharArray();
byte[] result = new byte[16];
for(int i = 0; i < result.length; ++i) {
if(i < chars.length)
result[i] = (byte)(chars[i] & 0x7F);
else
result[i] = (byte)' '; // Padding with spaces
}
return result;
}
}
public static enum EntryType {
DATA_FORK(EntryDescriptor.ENTRY_ID_DATA),
RESOURCE_FORK(EntryDescriptor.ENTRY_ID_RESOURCE),
REAL_NAME(EntryDescriptor.ENTRY_ID_REALNAME),
COMMENT(EntryDescriptor.ENTRY_ID_COMMENT),
ICON_BW(EntryDescriptor.ENTRY_ID_ICONBW),
ICON_COLOR(EntryDescriptor.ENTRY_ID_ICONCOLOR),
FILE_INFO(EntryDescriptor.ENTRY_ID_UNUSED),
FINDER_INFO(EntryDescriptor.ENTRY_ID_FINDERINFO);
private final int typeNumber;
private EntryType(int typeNumber) {
this.typeNumber = typeNumber;
}
public int getTypeNumber() { return typeNumber; }
}
private static final byte[] EMPTY_RESOURCE_FORK = {
(byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x1e,
(byte) 0x54, (byte) 0x68, (byte) 0x69, (byte) 0x73,
(byte) 0x20, (byte) 0x72, (byte) 0x65, (byte) 0x73,
(byte) 0x6f, (byte) 0x75, (byte) 0x72, (byte) 0x63,
(byte) 0x65, (byte) 0x20, (byte) 0x66, (byte) 0x6f,
(byte) 0x72, (byte) 0x6b, (byte) 0x20, (byte) 0x69,
(byte) 0x6e, (byte) 0x74, (byte) 0x65, (byte) 0x6e,
(byte) 0x74, (byte) 0x69, (byte) 0x6f, (byte) 0x6e,
(byte) 0x61, (byte) 0x6c, (byte) 0x6c, (byte) 0x79,
(byte) 0x20, (byte) 0x6c, (byte) 0x65, (byte) 0x66,
(byte) 0x74, (byte) 0x20, (byte) 0x62, (byte) 0x6c,
(byte) 0x61, (byte) 0x6e, (byte) 0x6b, (byte) 0x20,
(byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x1e,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x1c, (byte) 0x00, (byte) 0x1e,
(byte) 0xff, (byte) 0xff,
};
private static final int ALIGNMENT = 4096;
private final FileType fileType;
private final AppleSingleVersion version;
private final FileSystem homeFileSystem;
private final LinkedList<Pair<EntryType, AppleSingleEntry>> entryList =
new LinkedList<Pair<EntryType, AppleSingleEntry>>();
public AppleSingleBuilder(FileType fileType, AppleSingleVersion version, FileSystem homeFileSystem) {
if(fileType == null)
throw new IllegalArgumentException("fileType == null");
if(version == null)
throw new IllegalArgumentException("version == null");
if(homeFileSystem == null)
throw new IllegalArgumentException("homeFileSystem == null");
this.fileType = fileType;
this.version = version;
this.homeFileSystem = homeFileSystem;
}
public void addDataFork(byte[] resourceForkData) {
entryList.add(new Pair<EntryType, AppleSingleEntry>(EntryType.DATA_FORK,
new RawDataEntry(resourceForkData)));
}
public void addResourceFork(byte[] resourceForkData) {
entryList.add(new Pair<EntryType, AppleSingleEntry>(
EntryType.RESOURCE_FORK, new RawDataEntry(resourceForkData)));
}
public void addEmptyResourceFork() {
entryList.add(new Pair<EntryType, AppleSingleEntry>(
EntryType.RESOURCE_FORK,
new RawDataEntry(EMPTY_RESOURCE_FORK)));
}
public void addFinderInfo(byte[] finderInfoData,
List<Pair<String, byte[]>> extendedAttributeList)
{
if(finderInfoData != null && finderInfoData.length != 32) {
throw new IllegalArgumentException("Incorrect Finder info data " +
"length (expected: 32, actual: " + finderInfoData.length +
").");
}
ArrayList<Pair<byte[], byte[]>> attributeDataList =
new ArrayList<Pair<byte[], byte[]>>(
extendedAttributeList.size());
for(Pair<String, byte[]> extendedAttributePair : extendedAttributeList)
{
/* Note: Is normalization necessary? Other substitutions? */
byte[] attributeNameUtf8 =
Util.encodeString(extendedAttributePair.getA() + "\0",
"UTF-8");
if(attributeNameUtf8.length > 255) {
throw new RuntimeException("Extended attribute name " +
"\"" + extendedAttributePair.getA() + "\" is too " +
"long (maximum length: 255, actual length: " +
attributeNameUtf8.length + ").");
}
byte[] attributeData = extendedAttributePair.getB();
/* Because of limitations in the XNU kernel's AppleDouble
* implementation, the maximum data size of extended attributes
* stored inside an AppleDouble file is 128 KiB. */
if(attributeData.length > (128 * 1024)) {
throw new RuntimeException("Attribute data size is too large " +
"to be stored in an AppleDouble file (maximum " +
"allowed: " + (128 * 1024) + ", actual: " +
attributeData.length + ").");
}
attributeDataList.add(new Pair<byte[], byte[]>(attributeNameUtf8,
extendedAttributePair.getB()));
}
entryList.add(new Pair<EntryType, AppleSingleEntry>(
EntryType.FINDER_INFO,
new FinderInfoEntry(finderInfoData, attributeDataList)));
}
/**
* Serializes the current state of the builder into a valid AppleSingle data representation that
* can be written down to file.
*
* @return the data of an AppleSingle format file built from the current state of the builder.
*/
public byte[] getResult() {
int dataSize = AppleSingleHeader.length();
dataSize += EntryDescriptor.length()*entryList.size();
int dataStartOffset = dataSize;
for(Pair<EntryType, AppleSingleEntry> p : entryList) {
dataSize += p.getB().getBytes(dataSize, null, 0);
}
/* Adjust dataSize for alignment. */
int remainingAlignmentSize = (ALIGNMENT - (dataSize % ALIGNMENT));
dataSize = dataSize + remainingAlignmentSize;
byte[] result = new byte[dataSize];
int pointer = 0;
{
AppleSingleHeader header = new AppleSingleHeader(fileType.getMagic(),
version.getVersionNumber(), homeFileSystem, entryList.size());
byte[] headerData = header.getBytes();
System.arraycopy(headerData, 0, result, pointer, headerData.length);
pointer += headerData.length;
}
final EntryDescriptor[] entryDescriptors =
new EntryDescriptor[entryList.size()];
int i = 0;
int dataOffset = dataStartOffset;
for(Pair<EntryType, AppleSingleEntry> p : entryList) {
int entryDataLength = p.getB().getBytes(dataOffset, null, 0);
if(p.getA() == EntryType.FINDER_INFO) {
/* If we have a Finder info entry, then make sure that all the
* alignment padding is allocated to this entry. This is done in
* order to have headroom for expansion of extended attribute
* list/data. */
entryDataLength += remainingAlignmentSize;
remainingAlignmentSize = 0;
}
EntryDescriptor ed = new EntryDescriptor(p.getA().getTypeNumber(),
dataOffset, entryDataLength);
entryDescriptors[i++] = ed;
dataOffset += entryDataLength;
byte[] entryDescriptorData = ed.getBytes();
System.arraycopy(entryDescriptorData, 0, result, pointer, entryDescriptorData.length);
pointer += entryDescriptorData.length;
}
if(pointer != dataStartOffset)
throw new RuntimeException("Internal error: Miscalculation of dataStartOffset (should be: " +
pointer + ", was: " + dataStartOffset + ")");
i = 0;
for(Pair<EntryType, AppleSingleEntry> p : entryList) {
final EntryDescriptor desc = entryDescriptors[i++];
final int entryOffset = desc.getEntryOffset();
final int entryLength = desc.getEntryLength();
if(pointer != entryOffset) {
throw new RuntimeException("Internal error: Miscalculation " +
"of data offset for entry " + i + " (calculated: " +
entryOffset + ", actual: " + pointer + ").");
}
final int entryDataLength =
p.getB().getBytes(pointer, result, pointer);
pointer += entryDataLength;
final int entryPaddingLength = entryLength - entryDataLength;
if(entryPaddingLength != 0) {
/* Fill trailing bytes in entry with zeroed data. */
Arrays.fill(result, pointer, pointer + entryPaddingLength,
(byte) 0);
pointer += entryPaddingLength;
}
}
final int trailingPaddingLength = result.length - pointer;
if(trailingPaddingLength != 0) {
/* Fill trailing bytes in file with zeroed data. */
Arrays.fill(result, pointer, pointer + trailingPaddingLength,
(byte) 0);
pointer += trailingPaddingLength;
}
return result;
}
public interface AppleSingleEntry {
public int getBytes(long fileOffset, byte[] data, int offset);
}
public class RawDataEntry implements AppleSingleEntry {
private final byte[] rawData;
public RawDataEntry(byte[] rawData) {
this.rawData = rawData;
}
public int getBytes(long fileOffset, byte[] data, int offset) {
if(data != null) {
System.arraycopy(rawData, 0, data, offset, rawData.length);
}
return rawData.length;
}
}
public class FinderInfoEntry implements AppleSingleEntry {
private final byte[] finderInfoData;
private final ArrayList<Pair<byte[], byte[]>> attributeDataList;
private FinderInfoEntry(byte[] finderInfoData,
ArrayList<Pair<byte[], byte[]>> attributeDataList)
{
this.finderInfoData = finderInfoData;
this.attributeDataList = attributeDataList;
}
public int getBytes(long fileOffset, byte[] data, int offset) {
int finderInfoDataSize = 0;
if(data != null) {
if(finderInfoData != null) {
System.arraycopy(finderInfoData, 0, data,
offset + finderInfoDataSize, 32);
}
else {
Arrays.fill(data, offset + finderInfoDataSize,
offset + finderInfoDataSize + 32, (byte) 0);
}
}
finderInfoDataSize += 32;
if(!attributeDataList.isEmpty()) {
/* 2-byte padding. */
if(data != null) {
Arrays.fill(data, offset + finderInfoDataSize,
offset + finderInfoDataSize + 2, (byte) 0);
}
finderInfoDataSize += 2;
/* Calculate offsets of header and entries. This is non-trivial
* due to the requirement that entries are variable-sized and
* must be 4-byte aligned. */
final int attributeHeaderOffset = offset + finderInfoDataSize;
finderInfoDataSize += AttributeHeader.STRUCTSIZE;
final int[] attributeEntryOffsets =
data != null ? new int[attributeDataList.size()] : null;
int extendedAttributesDataSize = 0;
int i = 0;
for(Pair<byte[], byte[]> attributeData : attributeDataList) {
extendedAttributesDataSize += attributeData.getB().length;
/* Align to 4-byte boundaries. */
final int remainder =
(int) ((fileOffset + finderInfoDataSize) & 0x3);
if(remainder != 0) {
if(data != null) {
Arrays.fill(data, offset + finderInfoDataSize,
offset + finderInfoDataSize + remainder,
(byte) 0);
}
finderInfoDataSize += remainder;
}
if(data != null) {
attributeEntryOffsets[i++] =
offset + finderInfoDataSize;
}
finderInfoDataSize += AttributeEntry.STATIC_STRUCTSIZE +
attributeData.getA().length;
}
/* Header + entries must fit within the first 64 KiB of the file
* beacuse of limitations in the XNU AppleDouble
* implementation. */
if(fileOffset + finderInfoDataSize > 65536) {
throw new RuntimeException("Attribute entry list extends " +
"beyond the first 64k of the file (ends at: " +
(fileOffset + finderInfoDataSize) + ").");
}
final int extendedAttributesDataStart =
(int) (fileOffset + finderInfoDataSize);
int curDataOffset = finderInfoDataSize;
/* Write out attribute header. */
if(data != null) {
final AttributeHeader header = new AttributeHeader(0,
extendedAttributesDataStart +
extendedAttributesDataSize,
extendedAttributesDataStart,
extendedAttributesDataSize,
(short) 0, (short) attributeDataList.size());
final byte[] headerBytes = header.getBytes();
System.arraycopy(headerBytes, 0, data,
attributeHeaderOffset, headerBytes.length);
}
/* Write out attribute entry headers and associated data. */
i = 0;
for(Pair<byte[], byte[]> attributeData : attributeDataList) {
final byte[] content = attributeData.getB();
if(data != null) {
final byte[] name = attributeData.getA();
final AttributeEntry entry = new AttributeEntry(
curDataOffset, content.length, (short) 0, name,
0, (short) name.length);
final byte[] entryData = entry.getBytes();
/* Write out entry header. */
System.arraycopy(entryData, 0, data,
attributeEntryOffsets[i++], entryData.length);
/* Write out entry data. */
System.arraycopy(content, 0, data,
offset + finderInfoDataSize, content.length);
}
finderInfoDataSize += content.length;
}
}
return finderInfoDataSize;
}
}
}