Introducing Entry/EntryType/AppleSingleReader to make reading aspects

more common across tools.
This commit is contained in:
Rob Greene 2018-06-03 12:33:09 -05:00
parent b72c9f9b74
commit 4f34a8a289
7 changed files with 321 additions and 203 deletions

View File

@ -1,6 +1,5 @@
package io.github.applecommander.applesingle;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -11,7 +10,9 @@ import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@ -34,35 +35,16 @@ public class AppleSingle {
public static final int MAGIC_NUMBER = 0x0051600;
public static final int VERSION_NUMBER1 = 0x00010000;
public static final int VERSION_NUMBER2 = 0x00020000;
public static final Map<Integer,String> ENTRY_TYPE_NAMES = new HashMap<Integer,String>() {
private static final long serialVersionUID = 7142066556402030814L;
{
put(1, "Data Fork");
put(2, "Resource Fork");
put(3, "Real Name");
put(4, "Comment");
put(5, "Icon, B&W");
put(6, "Icon, Color");
put(7, "File Info");
put(8, "File Dates Info");
put(9, "Finder Info");
put(10, "Macintosh File Info");
put(11, "ProDOS File Info");
put(12, "MS-DOS File Info");
put(13, "Short Name");
put(14, "AFP File Info");
put(15, "Directory ID");
}};
private Map<Integer,Consumer<byte[]>> entryConsumers = new HashMap<>();
private Map<Integer,Consumer<Entry>> entryConsumers = new HashMap<>();
{
entryConsumers.put(1, this::setDataFork);
entryConsumers.put(2, this::setResourceFork);
entryConsumers.put(3, this::setRealName);
entryConsumers.put(8, this::setFileDatesInfo);
entryConsumers.put(11, this::setProdosFileInfo);
entryConsumers.put(1, entry -> this.dataFork = entry.getData());
entryConsumers.put(2, entry -> this.resourceFork = entry.getData());
entryConsumers.put(3, entry -> this.realName = Utilities.entryToAsciiString(entry));
entryConsumers.put(8, entry -> this.fileDatesInfo = FileDatesInfo.fromEntry(entry));
entryConsumers.put(11, entry -> this.prodosFileInfo = ProdosFileInfo.fromEntry(entry));
}
private byte[] dataFork;
private byte[] resourceFork;
private String realName;
@ -72,66 +54,13 @@ public class AppleSingle {
private AppleSingle() {
// Allow Builder construction
}
private AppleSingle(byte[] data) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
required(buffer, MAGIC_NUMBER, "Not an AppleSingle file - magic number does not match.");
required(buffer, VERSION_NUMBER2, "Only AppleSingle version 2 supported.");
buffer.position(buffer.position() + 16); // Skip filler
int entries = buffer.getShort();
for (int i = 0; i < entries; i++) {
int entryId = buffer.getInt();
int offset = buffer.getInt();
int length = buffer.getInt();
buffer.mark();
buffer.position(offset);
byte[] entryData = new byte[length];
buffer.get(entryData);
// Defer to the proper set method or crash if we don't support that type of entry
Optional.ofNullable(entryConsumers.get(entryId))
.orElseThrow(() -> new IOException(String.format("Unsupported entry type of %04X (%s)", entryId,
ENTRY_TYPE_NAMES.getOrDefault(entryId, "Unknown"))))
.accept(entryData);
buffer.reset();
}
}
private void required(ByteBuffer buffer, int expected, String message) throws IOException {
int actual = buffer.getInt();
if (actual != expected) {
throw new IOException(String.format("%s Expected 0x%08x but read 0x%08x.", message, expected, actual));
}
}
private void setDataFork(byte[] entryData) {
this.dataFork = entryData;
}
private void setResourceFork(byte[] entryData) {
this.resourceFork = entryData;
}
private void setRealName(byte[] entryData) {
for (int i=0; i<entryData.length; i++) {
entryData[i] = (byte)(entryData[i] & 0x7f);
}
this.realName = new String(entryData);
}
private void setProdosFileInfo(byte[] entryData) {
ByteBuffer infoData = ByteBuffer.wrap(entryData)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
int access = infoData.getShort();
int fileType = infoData.getShort();
int auxType = infoData.getInt();
this.prodosFileInfo = new ProdosFileInfo(access, fileType, auxType);
}
private void setFileDatesInfo(byte[] entryData) {
ByteBuffer infoData = ByteBuffer.wrap(entryData)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
int creation = infoData.getInt();
int modification = infoData.getInt();
int backup = infoData.getInt();
int access = infoData.getInt();
this.fileDatesInfo = new FileDatesInfo(creation, modification, backup, access);
private AppleSingle(List<Entry> entries) throws IOException {
entries.forEach(entry -> {
Optional.ofNullable(entry)
.map(Entry::getEntryId)
.map(entryConsumers::get)
.ifPresent(c -> c.accept(entry));
});
}
public byte[] getDataFork() {
@ -228,7 +157,7 @@ public class AppleSingle {
public static AppleSingle read(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream, "Please supply an input stream");
return read(AppleSingle.toByteArray(inputStream));
return read(Utilities.toByteArray(inputStream));
}
public static AppleSingle read(File file) throws IOException {
Objects.requireNonNull(file, "Please supply a file");
@ -236,11 +165,54 @@ public class AppleSingle {
}
public static AppleSingle read(Path path) throws IOException {
Objects.requireNonNull(path, "Please supply a file");
return new AppleSingle(Files.readAllBytes(path));
return read(Files.readAllBytes(path));
}
public static AppleSingle read(byte[] data) throws IOException {
Objects.requireNonNull(data);
return new AppleSingle(data);
return new AppleSingle(asEntries(data));
}
public static List<Entry> asEntries(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream);
return asEntries(Utilities.toByteArray(inputStream));
}
public static List<Entry> asEntries(File file) throws IOException {
Objects.requireNonNull(file);
return asEntries(file.toPath());
}
public static List<Entry> asEntries(Path path) throws IOException {
Objects.requireNonNull(path);
return asEntries(Files.readAllBytes(path));
}
public static List<Entry> asEntries(byte[] data) throws IOException {
Objects.requireNonNull(data);
return asEntries(AppleSingleReader.builder().data(data).build());
}
public static List<Entry> asEntries(AppleSingleReader reader) throws IOException {
Objects.requireNonNull(reader);
List<Entry> entries = new ArrayList<>();
required(reader, "Magic number", "Not an AppleSingle file - magic number does not match.", MAGIC_NUMBER);
int version = required(reader, "Version", "Only AppleSingle version 1 and 2 supported.", VERSION_NUMBER1, VERSION_NUMBER2);
reader.reportVersion(version);
reader.read(16, "Filler");
int numberOfEntries = reader.read(Short.BYTES, "Number of entries").getShort();
reader.reportNumberOfEntries(numberOfEntries);
for (int i = 0; i < numberOfEntries; i++) {
Entry entry = Entry.create(reader);
entries.add(entry);
reader.reportEntry(entry);
}
return entries;
}
private static int required(AppleSingleReader reader, String description, String message, int... expecteds) throws IOException {
int actual = reader.read(Integer.BYTES, description).getInt();
for (int expected : expecteds) {
if (actual == expected) return actual;
}
List<String> versions = new ArrayList<>();
for (int expected : expecteds) versions.add(String.format("0x%08x", expected));
throw new IOException(String.format("%s Expected %s but read 0x%08x.",
message, String.join(",", versions), actual));
}
public static Builder builder() {
@ -324,17 +296,4 @@ public class AppleSingle {
return as;
}
}
/** Utility method to read all bytes from an InputStream. May move if more utility methods appear. */
public static byte[] toByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while (true) {
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
if (len == -1) break;
outputStream.write(buf, 0, len);
}
outputStream.flush();
return outputStream.toByteArray();
}
}

View File

@ -0,0 +1,91 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
import java.util.function.Consumer;
public final class AppleSingleReader {
private byte[] data;
private int pos = 0;
private Consumer<Integer> versionReporter = v -> {};
private Consumer<Integer> numberOfEntriesReporter = n -> {};
private Consumer<Entry> entryReporter = e -> {};
private ReadAtReporter readAtReporter = (s,l,b,d) -> {};
public ByteBuffer read(int len, String description) {
try {
return readAt(pos, len, description);
} finally {
pos += len;
}
}
public ByteBuffer readAt(int start, int len, String description) {
byte[] chunk = new byte[len];
System.arraycopy(data, start, chunk, 0, len);
readAtReporter.accept(start, len, chunk, description);
ByteBuffer buffer = ByteBuffer.wrap(chunk)
.order(ByteOrder.BIG_ENDIAN);
return buffer;
}
public void reportVersion(int version) {
versionReporter.accept(version);
}
public void reportNumberOfEntries(int numberOfEntries) {
numberOfEntriesReporter.accept(numberOfEntries);
}
public void reportEntry(Entry entry) {
entryReporter.accept(entry);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private AppleSingleReader reader = new AppleSingleReader();
private Builder() {
// Prevent construction
}
public Builder data(byte[] data) {
Objects.requireNonNull(data);
reader.data = data;
return this;
}
public Builder versionReporter(Consumer<Integer> consumer) {
Objects.requireNonNull(consumer);
reader.versionReporter = reader.versionReporter.andThen(consumer);
return this;
}
public Builder numberOfEntriesReporter(Consumer<Integer> consumer) {
Objects.requireNonNull(consumer);
reader.numberOfEntriesReporter = reader.numberOfEntriesReporter.andThen(consumer);
return this;
}
public Builder entryReporter(Consumer<Entry> consumer) {
Objects.requireNonNull(consumer);
reader.entryReporter = reader.entryReporter.andThen(consumer);
return this;
}
public Builder readAtReporter(ReadAtReporter consumer) {
Objects.requireNonNull(consumer);
reader.readAtReporter = reader.readAtReporter.andThen(consumer);
return this;
}
public AppleSingleReader build() {
Objects.requireNonNull(reader.data, "You must supply a byte[] of data");
return reader;
}
}
/**
* A reporter for the {@code AppleSingleReader#readAt(int, int, String)} method,
* heaviliy modeled on the {@code Consumer} interface.
*/
public interface ReadAtReporter {
public void accept(int start, int len, byte[] data, String description);
default ReadAtReporter andThen(ReadAtReporter after) {
Objects.requireNonNull(after);
return (s,l,b,d) -> { accept(s,l,b,d); after.accept(s,l,b,d); };
}
}
}

View File

@ -0,0 +1,52 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
public class Entry {
public static final int LENGTH = 12;
private int entryId;
private int offset;
private int length;
private byte[] data;
public static Entry create(AppleSingleReader reader) {
Objects.requireNonNull(reader);
ByteBuffer buffer = reader.read(LENGTH, "Entry header");
Entry entry = new Entry();
entry.entryId = buffer.getInt();
entry.offset = buffer.getInt();
entry.length = buffer.getInt();
entry.data = reader.readAt(entry.offset, entry.length, EntryType.findNameOrUnknown(entry)).array();
return entry;
}
public static Entry create(EntryType type, byte[] data) {
Objects.requireNonNull(type);
Objects.requireNonNull(data);
Entry entry = new Entry();
entry.entryId = type.entryId;
entry.offset = -1;
entry.length = data.length;
entry.data = data;
return entry;
}
public int getEntryId() {
return entryId;
}
public int getOffset() {
return offset;
}
public int getLength() {
return length;
}
public byte[] getData() {
return data;
}
public ByteBuffer getBuffer() {
return ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).asReadOnlyBuffer();
}
}

View File

@ -0,0 +1,44 @@
package io.github.applecommander.applesingle;
public enum EntryType {
DATA_FORK(1, "Data Fork"),
RESOURCE_FORK(2, "Resource Fork"),
REAL_NAME(3, "Real Name"),
COMMENT(4, "Comment"),
ICON_BW(5, "Icon, B&W"),
ICON_COLOR(6, "Icon, Color"),
FILE_INFO(7, "File Info"),
FILE_DATES_INFO(8, "File Dates Info"),
FINDER_INFO(9, "Finder Info"),
MACINTOSH_FILE_INFO(10, "Macintosh File Info"),
PRODOS_FILE_INFO(11, "ProDOS File Info"),
MSDOS_FILE_INFO(12, "MS-DOS File Info"),
SHORT_NAME(13, "Short Name"),
AFP_FILE_INFO(14, "AFP File Info"),
DIRECTORY_ID(15, "Directory ID");
public static final String findNameOrUnknown(Entry entry) {
for (EntryType et : values()) {
if (et.entryId == entry.getEntryId()) {
return et.name;
}
}
return "Unknown";
}
public static final EntryType find(int entryId) {
for (EntryType et : values()) {
if (et.entryId == entryId) {
return et;
}
}
throw new IllegalArgumentException(String.format("Unable to find EntryType # %d", entryId));
}
public final int entryId;
public final String name;
private EntryType(int entryId, String name) {
this.entryId = entryId;
this.name= name;
}
}

View File

@ -1,5 +1,6 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.function.IntSupplier;
@ -18,6 +19,14 @@ public class FileDatesInfo {
public static int fromInstant(Instant instant) {
return (int)(instant.getEpochSecond() - EPOCH_INSTANT.getEpochSecond());
}
public static FileDatesInfo fromEntry(Entry entry) {
ByteBuffer infoData = entry.getBuffer();
int creation = infoData.getInt();
int modification = infoData.getInt();
int backup = infoData.getInt();
int access = infoData.getInt();
return new FileDatesInfo(creation, modification, backup, access);
}
public FileDatesInfo() {
int current = FileDatesInfo.fromInstant(Instant.now());

View File

@ -1,5 +1,7 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import io.github.applecommander.applesingle.AppleSingle.Builder;
/**
@ -16,6 +18,13 @@ public class ProdosFileInfo {
public static ProdosFileInfo standardBIN() {
return new ProdosFileInfo(0xc3, 0x06, 0x0000);
}
public static ProdosFileInfo fromEntry(Entry entry) {
ByteBuffer infoData = entry.getBuffer();
int access = infoData.getShort();
int fileType = infoData.getShort();
int auxType = infoData.getInt();
return new ProdosFileInfo(access, fileType, auxType);
}
public ProdosFileInfo(int access, int fileType, int auxType) {
this.access = access;

View File

@ -2,18 +2,23 @@ package io.github.applecommander.applesingle.tools.asu;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.AppleSingleReader;
import io.github.applecommander.applesingle.Entry;
import io.github.applecommander.applesingle.EntryType;
import io.github.applecommander.applesingle.FileDatesInfo;
import io.github.applecommander.applesingle.ProdosFileInfo;
import io.github.applecommander.applesingle.Utilities;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@ -43,29 +48,22 @@ public class AnalyzeCommand implements Callable<Void> {
@Override
public Void call() throws IOException {
byte[] fileData = stdinFlag ? AppleSingle.toByteArray(System.in) : Files.readAllBytes(path);
byte[] fileData = stdinFlag ? Utilities.toByteArray(System.in) : Files.readAllBytes(path);
if (verboseFlag) this.verbose = System.out;
State state = new State(fileData);
match(state, "Magic number", "Not an AppleSingle file - magic number does not match.",
AppleSingle.MAGIC_NUMBER);
int version = match(state, "Version", "Only recognize AppleSingle versions 1 and 2.",
AppleSingle.VERSION_NUMBER1, AppleSingle.VERSION_NUMBER2);
verbose.printf(" .. Version 0x%08x\n", version);
state.read(16, "Filler");
int numberOfEntries = state.read(Short.BYTES, "Number of entries").getShort();
verbose.printf(" .. Entries = %d\n", numberOfEntries);
List<Entry> entries = new ArrayList<>();
for (int i = 0; i < numberOfEntries; i++) {
ByteBuffer buffer = state.read(12, String.format("Entry #%d", i+1));
Entry entry = new Entry(i+1, buffer);
entry.print(verbose);
entries.add(entry);
}
entries.sort((a,b) -> Integer.compare(a.offset, b.offset));
for (Entry entry : entries) entryReport(state, entry);
List<IntRange> used = new ArrayList<>();
HexDumper dumper = HexDumper.standard();
AppleSingleReader reader = AppleSingleReader.builder()
.data(fileData)
.readAtReporter((start,len,b,d) -> used.add(IntRange.of(start, start+len)))
.readAtReporter((start,len,chunk,desc) -> dumper.dump(start, chunk, desc))
.versionReporter(this::reportVersion)
.numberOfEntriesReporter(this::reportNumberOfEntries)
.entryReporter(this::reportEntry)
.build();
AppleSingle.asEntries(reader);
List<IntRange> ranges = IntRange.normalize(state.used);
List<IntRange> ranges = IntRange.normalize(used);
if (ranges.size() == 1 && ranges.get(0).getLow() == 0 && ranges.get(0).getHigh() == fileData.length) {
verbose.printf("The entirety of the file was used.\n");
} else {
@ -74,99 +72,55 @@ public class AnalyzeCommand implements Callable<Void> {
}
return null;
}
public int match(State state, String description, String message, int... expecteds) throws IOException {
ByteBuffer buffer = state.read(Integer.BYTES, description);
int actual = buffer.getInt();
for (int expected : expecteds) {
if (actual == expected) return actual;
}
throw new IOException(String.format("%s Aborting.", message));
public void reportVersion(int version) {
verbose.printf(" .. %s\n", VERSION_TEXT.getOrDefault(version, "Unrecognized version!"));
}
public void entryReport(State state, Entry entry) throws IOException {
String entryName = AppleSingle.ENTRY_TYPE_NAMES.getOrDefault(entry.entryId, "Unknown");
ByteBuffer buffer = state.readAt(entry.offset, entry.length,
String.format("Entry #%d data (%s)", entry.index, entryName));
switch (entry.entryId) {
case 3:
case 4:
case 13:
displayEntryString(buffer, entryName);
break;
case 8:
displayFileDatesInfo(buffer, entryName);
break;
case 11:
displayProdosFileInfo(buffer, entryName);
break;
default:
verbose.printf(" .. No further details for this entry type (%s).\n", entryName);
break;
}
public void reportNumberOfEntries(int numberOfEntries) {
verbose.printf(" .. Number of entries = %d\n", numberOfEntries);
}
public void displayEntryString(ByteBuffer buffer, String entryName) {
StringBuilder sb = new StringBuilder();
while (buffer.hasRemaining()) {
int ch = Byte.toUnsignedInt(buffer.get()) & 0x7f;
sb.append((char)ch);
}
verbose.printf(" .. %s: '%s'\n", entryName, sb.toString());
public void reportEntry(Entry entry) {
String entryName = EntryType.findNameOrUnknown(entry);
verbose.printf(" .. Entry: entryId=%d (%s), offset=%d, length=%d\n", entry.getEntryId(),
entryName, entry.getOffset(), entry.getLength());
REPORTERS.getOrDefault(entry.getEntryId(), this::reportDefaultEntry)
.accept(entry, entryName);
}
public void displayFileDatesInfo(ByteBuffer buffer, String entryName) {
FileDatesInfo info = new FileDatesInfo(buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getInt());
private void reportDefaultEntry(Entry entry, String entryName) {
verbose.printf(" .. No further details for this entry type (%s).\n", entryName);
}
private void reportStringEntry(Entry entry, String entryName) {
verbose.printf(" .. %s: '%s'\n", entryName, Utilities.entryToAsciiString(entry));
}
private void reportFileDatesInfoEntry(Entry entry, String entryName) {
FileDatesInfo info = FileDatesInfo.fromEntry(entry);
verbose.printf(" .. %s -\n", entryName);
verbose.printf(" Creation: %s\n", info.getCreationInstant().toString());
verbose.printf(" Modification: %s\n", info.getModificationInstant().toString());
verbose.printf(" Backup: %s\n", info.getBackupInstant().toString());
verbose.printf(" Access: %s\n", info.getAccessInstant().toString());
}
public void displayProdosFileInfo(ByteBuffer buffer, String entryName) {
ProdosFileInfo info = new ProdosFileInfo(buffer.getShort(), buffer.getShort(), buffer.getInt());
private void reportProdosFileInfoEntry(Entry entry, String entryName) {
ProdosFileInfo info = ProdosFileInfo.fromEntry(entry);
verbose.printf(" .. %s -\n", entryName);
verbose.printf(" Access: %02X\n", info.getAccess());
verbose.printf(" File Type: %04X\n", info.getFileType());
verbose.printf(" Aux. Type: %04X\n", info.getAuxType());
}
public static class State {
private final byte[] data;
private int pos = 0;
private List<IntRange> used = new ArrayList<>();
private HexDumper dumper = HexDumper.standard();
public State(byte[] data) {
this.data = data;
}
public ByteBuffer read(int len, String description) throws IOException {
return readAt(pos, len, description);
}
public ByteBuffer readAt(int start, int len, String description) throws IOException {
byte[] chunk = new byte[len];
System.arraycopy(data, start, chunk, 0, len);
ByteBuffer buffer = ByteBuffer.wrap(chunk)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
dumper.dump(start, chunk, description);
used.add(IntRange.of(start, start+len));
pos= start+len;
return buffer;
}
}
public static class Entry {
private int index;
private int entryId;
private int offset;
private int length;
public Entry(int index, ByteBuffer buffer) {
this.index = index;
this.entryId = buffer.getInt();
this.offset = buffer.getInt();
this.length = buffer.getInt();
}
public void print(PrintStream ps) {
ps.printf(" .. Entry #%d, entryId=%d (%s), offset=%d, length=%d\n", index, entryId,
AppleSingle.ENTRY_TYPE_NAMES.getOrDefault(entryId, "Unknown"), offset, length);
private static final Map<Integer,String> VERSION_TEXT = new HashMap<Integer,String>() {
private static final long serialVersionUID = 7142066556402030814L;
{
put(AppleSingle.VERSION_NUMBER1, "Version 1");
put(AppleSingle.VERSION_NUMBER2, "Version 2");
}
};
private final Map<Integer,BiConsumer<Entry,String>> REPORTERS = new HashMap<Integer,BiConsumer<Entry,String>>();
{
REPORTERS.put(EntryType.REAL_NAME.entryId, this::reportStringEntry);
REPORTERS.put(EntryType.COMMENT.entryId, this::reportStringEntry);
REPORTERS.put(EntryType.SHORT_NAME.entryId, this::reportStringEntry);
REPORTERS.put(EntryType.FILE_DATES_INFO.entryId, this::reportFileDatesInfoEntry);
REPORTERS.put(EntryType.PRODOS_FILE_INFO.entryId, this::reportProdosFileInfoEntry);
}
}