package io.github.applecommander.applesingle;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Support reading of data from and AppleSingle source.
* Does not implement all components at this time, extend as required.
* All construction has been deferred to the read(...)
or {@link #builder()} methods.
*
* Currently supports entries:
* 1. Data Fork
* 2. Resource Fork
* 3. Real Name
* 11. ProDOS File Info
*
* @see AppleCommander issue #20
*/
public class AppleSingle {
public static final int MAGIC_NUMBER = 0x0051600;
public static final int VERSION_NUMBER = 0x00020000;
private Map> entryConsumers = new HashMap<>();
{
entryConsumers.put(1, this::setDataFork);
entryConsumers.put(2, this::setResourceFork);
entryConsumers.put(3, this::setRealName);
entryConsumers.put(11, this::setProdosFileInfo);
}
private byte[] dataFork;
private byte[] resourceFork;
private String realName;
private ProdosFileInfo prodosFileInfo = ProdosFileInfo.standardBIN();
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_NUMBER, "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.of(entryConsumers.get(entryId))
.orElseThrow(() -> new IOException(String.format("Unknown entry type of %04X", entryId)))
.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