mirror of
https://github.com/AppleCommander/applesingle.git
synced 2024-06-24 04:29:32 +00:00
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;
|
package io.github.applecommander.applesingle;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -11,7 +10,9 @@ import java.nio.ByteOrder;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -34,33 +35,14 @@ public class AppleSingle {
|
||||||
public static final int MAGIC_NUMBER = 0x0051600;
|
public static final int MAGIC_NUMBER = 0x0051600;
|
||||||
public static final int VERSION_NUMBER1 = 0x00010000;
|
public static final int VERSION_NUMBER1 = 0x00010000;
|
||||||
public static final int VERSION_NUMBER2 = 0x00020000;
|
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(1, entry -> this.dataFork = entry.getData());
|
||||||
entryConsumers.put(2, this::setResourceFork);
|
entryConsumers.put(2, entry -> this.resourceFork = entry.getData());
|
||||||
entryConsumers.put(3, this::setRealName);
|
entryConsumers.put(3, entry -> this.realName = Utilities.entryToAsciiString(entry));
|
||||||
entryConsumers.put(8, this::setFileDatesInfo);
|
entryConsumers.put(8, entry -> this.fileDatesInfo = FileDatesInfo.fromEntry(entry));
|
||||||
entryConsumers.put(11, this::setProdosFileInfo);
|
entryConsumers.put(11, entry -> this.prodosFileInfo = ProdosFileInfo.fromEntry(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] dataFork;
|
private byte[] dataFork;
|
||||||
|
@ -72,66 +54,13 @@ public class AppleSingle {
|
||||||
private AppleSingle() {
|
private AppleSingle() {
|
||||||
// Allow Builder construction
|
// Allow Builder construction
|
||||||
}
|
}
|
||||||
private AppleSingle(byte[] data) throws IOException {
|
private AppleSingle(List<Entry> entries) throws IOException {
|
||||||
ByteBuffer buffer = ByteBuffer.wrap(data)
|
entries.forEach(entry -> {
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
Optional.ofNullable(entry)
|
||||||
.asReadOnlyBuffer();
|
.map(Entry::getEntryId)
|
||||||
required(buffer, MAGIC_NUMBER, "Not an AppleSingle file - magic number does not match.");
|
.map(entryConsumers::get)
|
||||||
required(buffer, VERSION_NUMBER2, "Only AppleSingle version 2 supported.");
|
.ifPresent(c -> c.accept(entry));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getDataFork() {
|
public byte[] getDataFork() {
|
||||||
|
@ -228,7 +157,7 @@ public class AppleSingle {
|
||||||
|
|
||||||
public static AppleSingle read(InputStream inputStream) throws IOException {
|
public static AppleSingle read(InputStream inputStream) throws IOException {
|
||||||
Objects.requireNonNull(inputStream, "Please supply an input stream");
|
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 {
|
public static AppleSingle read(File file) throws IOException {
|
||||||
Objects.requireNonNull(file, "Please supply a file");
|
Objects.requireNonNull(file, "Please supply a file");
|
||||||
|
@ -236,11 +165,54 @@ public class AppleSingle {
|
||||||
}
|
}
|
||||||
public static AppleSingle read(Path path) throws IOException {
|
public static AppleSingle read(Path path) throws IOException {
|
||||||
Objects.requireNonNull(path, "Please supply a file");
|
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 {
|
public static AppleSingle read(byte[] data) throws IOException {
|
||||||
Objects.requireNonNull(data);
|
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() {
|
public static Builder builder() {
|
||||||
|
@ -324,17 +296,4 @@ public class AppleSingle {
|
||||||
return as;
|
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;
|
package io.github.applecommander.applesingle;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.function.IntSupplier;
|
import java.util.function.IntSupplier;
|
||||||
|
|
||||||
|
@ -18,6 +19,14 @@ public class FileDatesInfo {
|
||||||
public static int fromInstant(Instant instant) {
|
public static int fromInstant(Instant instant) {
|
||||||
return (int)(instant.getEpochSecond() - EPOCH_INSTANT.getEpochSecond());
|
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() {
|
public FileDatesInfo() {
|
||||||
int current = FileDatesInfo.fromInstant(Instant.now());
|
int current = FileDatesInfo.fromInstant(Instant.now());
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package io.github.applecommander.applesingle;
|
package io.github.applecommander.applesingle;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
import io.github.applecommander.applesingle.AppleSingle.Builder;
|
import io.github.applecommander.applesingle.AppleSingle.Builder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,6 +18,13 @@ public class ProdosFileInfo {
|
||||||
public static ProdosFileInfo standardBIN() {
|
public static ProdosFileInfo standardBIN() {
|
||||||
return new ProdosFileInfo(0xc3, 0x06, 0x0000);
|
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) {
|
public ProdosFileInfo(int access, int fileType, int auxType) {
|
||||||
this.access = access;
|
this.access = access;
|
||||||
|
|
|
@ -2,18 +2,23 @@ package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import io.github.applecommander.applesingle.AppleSingle;
|
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.FileDatesInfo;
|
||||||
import io.github.applecommander.applesingle.ProdosFileInfo;
|
import io.github.applecommander.applesingle.ProdosFileInfo;
|
||||||
|
import io.github.applecommander.applesingle.Utilities;
|
||||||
import picocli.CommandLine.Command;
|
import picocli.CommandLine.Command;
|
||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
import picocli.CommandLine.Parameters;
|
import picocli.CommandLine.Parameters;
|
||||||
|
@ -43,29 +48,22 @@ public class AnalyzeCommand implements Callable<Void> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Void call() throws IOException {
|
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;
|
if (verboseFlag) this.verbose = System.out;
|
||||||
|
|
||||||
State state = new State(fileData);
|
List<IntRange> used = new ArrayList<>();
|
||||||
match(state, "Magic number", "Not an AppleSingle file - magic number does not match.",
|
HexDumper dumper = HexDumper.standard();
|
||||||
AppleSingle.MAGIC_NUMBER);
|
AppleSingleReader reader = AppleSingleReader.builder()
|
||||||
int version = match(state, "Version", "Only recognize AppleSingle versions 1 and 2.",
|
.data(fileData)
|
||||||
AppleSingle.VERSION_NUMBER1, AppleSingle.VERSION_NUMBER2);
|
.readAtReporter((start,len,b,d) -> used.add(IntRange.of(start, start+len)))
|
||||||
verbose.printf(" .. Version 0x%08x\n", version);
|
.readAtReporter((start,len,chunk,desc) -> dumper.dump(start, chunk, desc))
|
||||||
state.read(16, "Filler");
|
.versionReporter(this::reportVersion)
|
||||||
int numberOfEntries = state.read(Short.BYTES, "Number of entries").getShort();
|
.numberOfEntriesReporter(this::reportNumberOfEntries)
|
||||||
verbose.printf(" .. Entries = %d\n", numberOfEntries);
|
.entryReporter(this::reportEntry)
|
||||||
List<Entry> entries = new ArrayList<>();
|
.build();
|
||||||
for (int i = 0; i < numberOfEntries; i++) {
|
AppleSingle.asEntries(reader);
|
||||||
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> 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) {
|
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");
|
verbose.printf("The entirety of the file was used.\n");
|
||||||
} else {
|
} else {
|
||||||
|
@ -75,98 +73,54 @@ public class AnalyzeCommand implements Callable<Void> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int match(State state, String description, String message, int... expecteds) throws IOException {
|
public void reportVersion(int version) {
|
||||||
ByteBuffer buffer = state.read(Integer.BYTES, description);
|
verbose.printf(" .. %s\n", VERSION_TEXT.getOrDefault(version, "Unrecognized version!"));
|
||||||
int actual = buffer.getInt();
|
|
||||||
for (int expected : expecteds) {
|
|
||||||
if (actual == expected) return actual;
|
|
||||||
}
|
}
|
||||||
throw new IOException(String.format("%s Aborting.", message));
|
public void reportNumberOfEntries(int numberOfEntries) {
|
||||||
|
verbose.printf(" .. Number of entries = %d\n", numberOfEntries);
|
||||||
}
|
}
|
||||||
|
public void reportEntry(Entry entry) {
|
||||||
public void entryReport(State state, Entry entry) throws IOException {
|
String entryName = EntryType.findNameOrUnknown(entry);
|
||||||
String entryName = AppleSingle.ENTRY_TYPE_NAMES.getOrDefault(entry.entryId, "Unknown");
|
verbose.printf(" .. Entry: entryId=%d (%s), offset=%d, length=%d\n", entry.getEntryId(),
|
||||||
ByteBuffer buffer = state.readAt(entry.offset, entry.length,
|
entryName, entry.getOffset(), entry.getLength());
|
||||||
String.format("Entry #%d data (%s)", entry.index, entryName));
|
REPORTERS.getOrDefault(entry.getEntryId(), this::reportDefaultEntry)
|
||||||
switch (entry.entryId) {
|
.accept(entry, entryName);
|
||||||
case 3:
|
}
|
||||||
case 4:
|
private void reportDefaultEntry(Entry entry, String entryName) {
|
||||||
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);
|
verbose.printf(" .. No further details for this entry type (%s).\n", entryName);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
private void reportStringEntry(Entry entry, String entryName) {
|
||||||
|
verbose.printf(" .. %s: '%s'\n", entryName, Utilities.entryToAsciiString(entry));
|
||||||
}
|
}
|
||||||
public void displayEntryString(ByteBuffer buffer, String entryName) {
|
private void reportFileDatesInfoEntry(Entry entry, String entryName) {
|
||||||
StringBuilder sb = new StringBuilder();
|
FileDatesInfo info = FileDatesInfo.fromEntry(entry);
|
||||||
while (buffer.hasRemaining()) {
|
|
||||||
int ch = Byte.toUnsignedInt(buffer.get()) & 0x7f;
|
|
||||||
sb.append((char)ch);
|
|
||||||
}
|
|
||||||
verbose.printf(" .. %s: '%s'\n", entryName, sb.toString());
|
|
||||||
}
|
|
||||||
public void displayFileDatesInfo(ByteBuffer buffer, String entryName) {
|
|
||||||
FileDatesInfo info = new FileDatesInfo(buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getInt());
|
|
||||||
verbose.printf(" .. %s -\n", entryName);
|
verbose.printf(" .. %s -\n", entryName);
|
||||||
verbose.printf(" Creation: %s\n", info.getCreationInstant().toString());
|
verbose.printf(" Creation: %s\n", info.getCreationInstant().toString());
|
||||||
verbose.printf(" Modification: %s\n", info.getModificationInstant().toString());
|
verbose.printf(" Modification: %s\n", info.getModificationInstant().toString());
|
||||||
verbose.printf(" Backup: %s\n", info.getBackupInstant().toString());
|
verbose.printf(" Backup: %s\n", info.getBackupInstant().toString());
|
||||||
verbose.printf(" Access: %s\n", info.getAccessInstant().toString());
|
verbose.printf(" Access: %s\n", info.getAccessInstant().toString());
|
||||||
}
|
}
|
||||||
public void displayProdosFileInfo(ByteBuffer buffer, String entryName) {
|
private void reportProdosFileInfoEntry(Entry entry, String entryName) {
|
||||||
ProdosFileInfo info = new ProdosFileInfo(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
ProdosFileInfo info = ProdosFileInfo.fromEntry(entry);
|
||||||
verbose.printf(" .. %s -\n", entryName);
|
verbose.printf(" .. %s -\n", entryName);
|
||||||
verbose.printf(" Access: %02X\n", info.getAccess());
|
verbose.printf(" Access: %02X\n", info.getAccess());
|
||||||
verbose.printf(" File Type: %04X\n", info.getFileType());
|
verbose.printf(" File Type: %04X\n", info.getFileType());
|
||||||
verbose.printf(" Aux. Type: %04X\n", info.getAuxType());
|
verbose.printf(" Aux. Type: %04X\n", info.getAuxType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class State {
|
private static final Map<Integer,String> VERSION_TEXT = new HashMap<Integer,String>() {
|
||||||
private final byte[] data;
|
private static final long serialVersionUID = 7142066556402030814L;
|
||||||
private int pos = 0;
|
{
|
||||||
private List<IntRange> used = new ArrayList<>();
|
put(AppleSingle.VERSION_NUMBER1, "Version 1");
|
||||||
private HexDumper dumper = HexDumper.standard();
|
put(AppleSingle.VERSION_NUMBER2, "Version 2");
|
||||||
|
|
||||||
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 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
Block a user