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