Introducing Entry/EntryType/AppleSingleReader to make reading aspects
more common across tools.
This commit is contained in:
parent
b72c9f9b74
commit
4f34a8a289
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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); };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue