Adding analyze command to 'asu'; adding support to API for date
information.
This commit is contained in:
parent
fbab3b426e
commit
03095785d5
|
@ -10,6 +10,7 @@ import java.nio.ByteBuffer;
|
|||
import java.nio.ByteOrder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -31,13 +32,34 @@ import java.util.function.Consumer;
|
|||
*/
|
||||
public class AppleSingle {
|
||||
public static final int MAGIC_NUMBER = 0x0051600;
|
||||
public static final int VERSION_NUMBER = 0x00020000;
|
||||
|
||||
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<>();
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -45,6 +67,7 @@ public class AppleSingle {
|
|||
private byte[] resourceFork;
|
||||
private String realName;
|
||||
private ProdosFileInfo prodosFileInfo = ProdosFileInfo.standardBIN();
|
||||
private FileDatesInfo fileDatesInfo = new FileDatesInfo();
|
||||
|
||||
private AppleSingle() {
|
||||
// Allow Builder construction
|
||||
|
@ -54,7 +77,7 @@ public class AppleSingle {
|
|||
.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.");
|
||||
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++) {
|
||||
|
@ -67,7 +90,8 @@ public class AppleSingle {
|
|||
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("Unknown entry type of %04X", entryId)))
|
||||
.orElseThrow(() -> new IOException(String.format("Unsupported entry type of %04X (%s)", entryId,
|
||||
ENTRY_TYPE_NAMES.getOrDefault(entryId, "Unknown"))))
|
||||
.accept(entryData);
|
||||
buffer.reset();
|
||||
}
|
||||
|
@ -99,6 +123,16 @@ public class AppleSingle {
|
|||
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() {
|
||||
return dataFork;
|
||||
|
@ -112,25 +146,31 @@ public class AppleSingle {
|
|||
public ProdosFileInfo getProdosFileInfo() {
|
||||
return prodosFileInfo;
|
||||
}
|
||||
public FileDatesInfo getFileDatesInfo() {
|
||||
return fileDatesInfo;
|
||||
}
|
||||
|
||||
public void save(OutputStream outputStream) throws IOException {
|
||||
final boolean hasResourceFork = resourceFork == null ? false : true;
|
||||
final boolean hasRealName = realName == null ? false : true;
|
||||
final int entries = 2 + (hasRealName ? 1 : 0) + (hasResourceFork ? 1 : 0);
|
||||
final boolean hasResourceFork = Objects.nonNull(resourceFork);
|
||||
final boolean hasRealName = Objects.nonNull(realName);
|
||||
final int entries = 3 + (hasRealName ? 1 : 0) + (hasResourceFork ? 1 : 0);
|
||||
|
||||
int realNameOffset = 26 + (12 * entries);
|
||||
int prodosFileInfoOffset = realNameOffset + (hasRealName ? realName.length() : 0);
|
||||
int resourceForkOffset = prodosFileInfoOffset + 8;
|
||||
int fileDatesInfoOffset = prodosFileInfoOffset + 8;
|
||||
int resourceForkOffset = fileDatesInfoOffset + 16;
|
||||
int dataForkOffset = resourceForkOffset + (hasResourceFork ? resourceFork.length : 0);
|
||||
|
||||
writeFileHeader(outputStream, entries);
|
||||
if (hasRealName) writeHeader(outputStream, 3, realNameOffset, realName.length());
|
||||
writeHeader(outputStream, 11, prodosFileInfoOffset, 8);
|
||||
writeHeader(outputStream, 8, fileDatesInfoOffset, 16);
|
||||
if (hasResourceFork) writeHeader(outputStream, 2, resourceForkOffset, resourceFork.length);
|
||||
writeHeader(outputStream, 1, dataForkOffset, dataFork.length);
|
||||
|
||||
if (hasRealName) writeRealName(outputStream);
|
||||
writeProdosFileInfo(outputStream);
|
||||
writeFileDatesInfo(outputStream);
|
||||
if (hasResourceFork) writeResourceFork(outputStream);
|
||||
writeDataFork(outputStream);
|
||||
}
|
||||
|
@ -149,7 +189,7 @@ public class AppleSingle {
|
|||
final byte[] filler = new byte[16];
|
||||
ByteBuffer buf = ByteBuffer.allocate(26).order(ByteOrder.BIG_ENDIAN);
|
||||
buf.putInt(MAGIC_NUMBER);
|
||||
buf.putInt(VERSION_NUMBER);
|
||||
buf.putInt(VERSION_NUMBER2);
|
||||
buf.put(filler);
|
||||
buf.putShort((short)numberOfEntries);
|
||||
outputStream.write(buf.array());
|
||||
|
@ -171,6 +211,14 @@ public class AppleSingle {
|
|||
buf.putInt(prodosFileInfo.auxType);
|
||||
outputStream.write(buf.array());
|
||||
}
|
||||
private void writeFileDatesInfo(OutputStream outputStream) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(16).order(ByteOrder.BIG_ENDIAN);
|
||||
buf.putInt(fileDatesInfo.getCreation());
|
||||
buf.putInt(fileDatesInfo.getModification());
|
||||
buf.putInt(fileDatesInfo.getBackup());
|
||||
buf.putInt(fileDatesInfo.getAccess());
|
||||
outputStream.write(buf.array());
|
||||
}
|
||||
private void writeResourceFork(OutputStream outputStream) throws IOException {
|
||||
outputStream.write(resourceFork);
|
||||
}
|
||||
|
@ -237,6 +285,41 @@ public class AppleSingle {
|
|||
as.prodosFileInfo.auxType = auxType;
|
||||
return this;
|
||||
}
|
||||
public Builder creationDate(int creation) {
|
||||
as.fileDatesInfo.creation = creation;
|
||||
return this;
|
||||
}
|
||||
public Builder creationDate(Instant creation) {
|
||||
as.fileDatesInfo.creation = FileDatesInfo.fromInstant(creation);
|
||||
return this;
|
||||
}
|
||||
public Builder modificationDate(int modification) {
|
||||
as.fileDatesInfo.modification = modification;
|
||||
return this;
|
||||
}
|
||||
public Builder modificationDate(Instant modification) {
|
||||
as.fileDatesInfo.modification = FileDatesInfo.fromInstant(modification);
|
||||
return this;
|
||||
}
|
||||
public Builder backupDate(int backup) {
|
||||
as.fileDatesInfo.backup = backup;
|
||||
return this;
|
||||
}
|
||||
public Builder backupDate(Instant backup) {
|
||||
as.fileDatesInfo.backup = FileDatesInfo.fromInstant(backup);
|
||||
return this;
|
||||
}
|
||||
public Builder accessDate(int access) {
|
||||
as.fileDatesInfo.access = access;
|
||||
return this;
|
||||
}
|
||||
public Builder accessDate(Instant access) {
|
||||
as.fileDatesInfo.access = FileDatesInfo.fromInstant(access);
|
||||
return this;
|
||||
}
|
||||
public Builder allDates(Instant instant) {
|
||||
return creationDate(instant).modificationDate(instant).backupDate(instant).accessDate(instant);
|
||||
}
|
||||
public AppleSingle build() {
|
||||
return as;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package io.github.applecommander.applesingle;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
public class FileDatesInfo {
|
||||
/** The number of seconds at the begining of the AppleSingle date epoch since the Unix epoch began. */
|
||||
//public static final int EPOCH_2000 = 946684800;
|
||||
public static final Instant EPOCH_INSTANT = Instant.parse("2000-01-01T00:00:00.00Z");
|
||||
/** Per the AppleSingle technical notes. */
|
||||
public static final int UNKNOWN_DATE = 0x80000000;
|
||||
|
||||
int creation;
|
||||
int modification;
|
||||
int backup;
|
||||
int access;
|
||||
|
||||
public static int fromInstant(Instant instant) {
|
||||
return (int)(instant.getEpochSecond() - EPOCH_INSTANT.getEpochSecond());
|
||||
}
|
||||
|
||||
public FileDatesInfo() {
|
||||
int current = FileDatesInfo.fromInstant(Instant.now());
|
||||
this.creation = current;
|
||||
this.modification = current;
|
||||
this.backup = current;
|
||||
this.access = current;
|
||||
}
|
||||
public FileDatesInfo(int creation, int modification, int backup, int access) {
|
||||
this.creation = creation;
|
||||
this.modification = modification;
|
||||
this.backup = backup;
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
public Instant getCreationInstant() {
|
||||
return toInstant(this::getCreation);
|
||||
}
|
||||
public Instant getModificationInstant() {
|
||||
return toInstant(this::getModification);
|
||||
}
|
||||
public Instant getBackupInstant() {
|
||||
return toInstant(this::getBackup);
|
||||
}
|
||||
public Instant getAccessInstant() {
|
||||
return toInstant(this::getAccess);
|
||||
}
|
||||
|
||||
/** Utility method to convert the int to a valid Unix epoch and Java Instant. */
|
||||
public Instant toInstant(IntSupplier timeSupplier) {
|
||||
return Instant.ofEpochSecond(timeSupplier.getAsInt() + EPOCH_INSTANT.getEpochSecond());
|
||||
}
|
||||
|
||||
public int getCreation() {
|
||||
return creation;
|
||||
}
|
||||
public int getModification() {
|
||||
return modification;
|
||||
}
|
||||
public int getBackup() {
|
||||
return backup;
|
||||
}
|
||||
public int getAccess() {
|
||||
return access;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import static org.junit.Assert.assertNull;
|
|||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -32,11 +34,14 @@ public class AppleSingleTest {
|
|||
public void testCreateAndReadAppleSingle() throws IOException {
|
||||
final byte[] dataFork = "testing testing 1-2-3".getBytes();
|
||||
final String realName = "test.as";
|
||||
// Need to truncate to seconds as the AppleSingle format is only good to seconds!
|
||||
final Instant instant = Instant.now().truncatedTo(ChronoUnit.SECONDS);
|
||||
|
||||
// Using default ProDOS info and skipping resource fork
|
||||
AppleSingle createdAS = AppleSingle.builder()
|
||||
.dataFork(dataFork)
|
||||
.realName(realName)
|
||||
.allDates(instant)
|
||||
.build();
|
||||
assertNotNull(createdAS);
|
||||
assertEquals(realName.toUpperCase(), createdAS.getRealName());
|
||||
|
@ -54,6 +59,11 @@ public class AppleSingleTest {
|
|||
assertArrayEquals(dataFork, readAS.getDataFork());
|
||||
assertNull(readAS.getResourceFork());
|
||||
assertNotNull(readAS.getProdosFileInfo());
|
||||
assertNotNull(readAS.getFileDatesInfo());
|
||||
assertEquals(instant, readAS.getFileDatesInfo().getCreationInstant());
|
||||
assertEquals(instant, readAS.getFileDatesInfo().getModificationInstant());
|
||||
assertEquals(instant, readAS.getFileDatesInfo().getAccessInstant());
|
||||
assertEquals(instant, readAS.getFileDatesInfo().getBackupInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Universal applesingle version number. Used for:
|
||||
# - Naming JAR file.
|
||||
# - The build will insert this into a file that is read at run time as well.
|
||||
version=1.0.1
|
||||
version=1.1.0
|
||||
|
||||
# Maven Central Repository G and A of GAV coordinate. :-)
|
||||
group=net.sf.applecommander
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
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.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import io.github.applecommander.applesingle.AppleSingle;
|
||||
import io.github.applecommander.applesingle.FileDatesInfo;
|
||||
import io.github.applecommander.applesingle.ProdosFileInfo;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
/**
|
||||
* Perform a bit of analysis of an AppleSingle archive to help extend the library, fix bugs,
|
||||
* or understand incompatibilities.
|
||||
*/
|
||||
@Command(name = "analyze", description = { "Perform an analysis on an AppleSingle file",
|
||||
"Please include a file name or indicate stdin should be read, but not both." },
|
||||
parameterListHeading = "%nParameters:%n",
|
||||
descriptionHeading = "%n",
|
||||
optionListHeading = "%nOptions:%n")
|
||||
public class AnalyzeCommand implements Callable<Void> {
|
||||
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
|
||||
private boolean helpFlag;
|
||||
|
||||
@Option(names = "--stdin", description = "Read AppleSingle from stdin.")
|
||||
private boolean stdinFlag;
|
||||
|
||||
@Option(names = { "-v", "--verbose" }, description = "Be verbose.")
|
||||
private boolean verboseFlag;
|
||||
private PrintStream verbose = new PrintStream(NullOutputStream.INSTANCE);
|
||||
|
||||
@Parameters(arity = "0..1", description = "File to process")
|
||||
private Path path;
|
||||
|
||||
@Override
|
||||
public Void call() throws IOException {
|
||||
byte[] fileData = stdinFlag ? AppleSingle.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> ranges = IntRange.normalize(state.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 {
|
||||
verbose.printf("Parts of the file were skipped!\n - Expected: %s\n - Actual: %s\n",
|
||||
Arrays.asList(IntRange.of(0,fileData.length)), ranges);
|
||||
}
|
||||
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 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 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 displayFileDatesInfo(ByteBuffer buffer, String entryName) {
|
||||
FileDatesInfo info = new FileDatesInfo(buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getInt());
|
||||
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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ package io.github.applecommander.applesingle.tools.asu;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import io.github.applecommander.applesingle.AppleSingle;
|
||||
|
@ -16,6 +19,10 @@ import picocli.CommandLine.Parameters;
|
|||
@Command(name = "create", description = { "Create an AppleSingle file" },
|
||||
parameterListHeading = "%nParameters:%n",
|
||||
descriptionHeading = "%n",
|
||||
footerHeading = "%nNotes:%n",
|
||||
footer = { "* Dates should be supplied like '2007-12-03T10:15:30.00Z'.",
|
||||
"* 'Known' ProDOS file types: TXT, BIN, INT, BAS, REL, SYS.",
|
||||
"* Include the output file or specify stdout" },
|
||||
optionListHeading = "%nOptions:%n")
|
||||
public class CreateCommand implements Callable<Void> {
|
||||
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
|
||||
|
@ -42,11 +49,20 @@ public class CreateCommand implements Callable<Void> {
|
|||
@Option(names = "--access", description = "Set the ProDOS access flags", converter = IntegerTypeConverter.class)
|
||||
private Integer access;
|
||||
|
||||
@Option(names = "--filetype", description = "Set the ProDOS file type (also accepts BIN/BAS/SYS)", converter = ProdosFileTypeConverter.class)
|
||||
@Option(names = "--filetype", description = "Set the ProDOS file type", converter = ProdosFileTypeConverter.class)
|
||||
private Integer filetype;
|
||||
|
||||
@Option(names = "--auxtype", description = "Set the ProDOS auxtype", converter = IntegerTypeConverter.class)
|
||||
private Integer auxtype;
|
||||
|
||||
@Option(names = "--creation-date", description = "Set the file creation date")
|
||||
private Instant creationDate;
|
||||
@Option(names = "--modification-date", description = "Set the file modification date")
|
||||
private Instant modificationDate;
|
||||
@Option(names = "--backup-date", description = "Set the file backup date")
|
||||
private Instant backupDate;
|
||||
@Option(names = "--access-date", description = "Set the file access date")
|
||||
private Instant accessDate;
|
||||
|
||||
@Parameters(arity = "0..1", description = "AppleSingle file to create")
|
||||
private Path file;
|
||||
|
@ -107,7 +123,7 @@ public class CreateCommand implements Callable<Void> {
|
|||
return resourceFork;
|
||||
}
|
||||
|
||||
public AppleSingle buildAppleSingle(byte[] dataFork, byte[] resourceFork) {
|
||||
public AppleSingle buildAppleSingle(byte[] dataFork, byte[] resourceFork) throws IOException {
|
||||
AppleSingle.Builder builder = AppleSingle.builder();
|
||||
if (realName != null) {
|
||||
builder.realName(realName);
|
||||
|
@ -121,6 +137,18 @@ public class CreateCommand implements Callable<Void> {
|
|||
if (dataFork != null) builder.dataFork(dataFork);
|
||||
if (resourceFork != null) builder.resourceFork(resourceFork);
|
||||
|
||||
if (dataForkFile != null || resourceForkFile != null) {
|
||||
Path path = Optional.ofNullable(dataForkFile).orElse(resourceForkFile);
|
||||
BasicFileAttributes attribs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
builder.creationDate(attribs.creationTime().toInstant());
|
||||
builder.modificationDate(attribs.lastModifiedTime().toInstant());
|
||||
builder.accessDate(attribs.lastAccessTime().toInstant());
|
||||
}
|
||||
if (creationDate != null) builder.creationDate(creationDate);
|
||||
if (modificationDate != null) builder.modificationDate(modificationDate);
|
||||
if (backupDate != null) builder.backupDate(backupDate);
|
||||
if (accessDate != null) builder.accessDate(accessDate);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package io.github.applecommander.applesingle.tools.asu;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** A slightly-configurable reusable hex dumping mechanism. */
|
||||
public class HexDumper {
|
||||
private PrintStream ps = System.out;
|
||||
private int lineWidth = 16;
|
||||
private LinePrinter printLine;
|
||||
|
||||
public static HexDumper standard() {
|
||||
HexDumper hd = new HexDumper();
|
||||
hd.printLine = hd::standardLine;
|
||||
return hd;
|
||||
}
|
||||
public static HexDumper alternate(LinePrinter linePrinter) {
|
||||
HexDumper hd = new HexDumper();
|
||||
hd.printLine = linePrinter;
|
||||
return hd;
|
||||
}
|
||||
|
||||
private HexDumper() {
|
||||
// Prevent construction
|
||||
}
|
||||
|
||||
public void dump(int address, byte[] data, String description) {
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
byte[] line = Arrays.copyOfRange(data, offset, Math.min(offset+lineWidth,data.length));
|
||||
printLine.print(address+offset, line, description);
|
||||
description = ""; // Only on first line!
|
||||
offset += line.length;
|
||||
}
|
||||
}
|
||||
|
||||
public void standardLine(int address, byte[] data, String description) {
|
||||
ps.printf("%04x: ", address);
|
||||
for (int i=0; i<lineWidth; i++) {
|
||||
if (i < data.length) {
|
||||
ps.printf("%02x ", data[i]);
|
||||
} else {
|
||||
ps.printf(".. ");
|
||||
}
|
||||
}
|
||||
ps.print("| ");
|
||||
for (int i=0; i<lineWidth; i++) {
|
||||
char ch = ' ';
|
||||
if (i < data.length) {
|
||||
byte b = data[i];
|
||||
ch = (b >= ' ') ? (char)b : '.';
|
||||
}
|
||||
ps.printf("%c", ch);
|
||||
}
|
||||
ps.printf(" | %s\n", description);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface LinePrinter {
|
||||
public void print(int address, byte[] data, String description);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package io.github.applecommander.applesingle.tools.asu;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* A basic integer range used to track file usage.
|
||||
* <code>low</code> is inclusive while <code>high</code> is exclusive, because it made
|
||||
* the code "simpler".
|
||||
*
|
||||
* @author rob
|
||||
*/
|
||||
public class IntRange {
|
||||
private int low;
|
||||
private int high;
|
||||
|
||||
/** Create an integer range. */
|
||||
public static IntRange of(int low, int high) {
|
||||
if (low == high) throw new UnsupportedOperationException("low and high cannot be the same");
|
||||
return new IntRange(Math.min(low,high), Math.max(low,high));
|
||||
}
|
||||
/** Normalize a list by combining all integer ranges that match. */
|
||||
public static List<IntRange> normalize(List<IntRange> ranges) {
|
||||
Stack<IntRange> rangeStack = new Stack<>();
|
||||
ranges.stream()
|
||||
.sorted((a,b) -> Integer.compare(a.low, b.low))
|
||||
.forEach(r -> {
|
||||
if (rangeStack.isEmpty()) {
|
||||
rangeStack.add(r);
|
||||
} else {
|
||||
rangeStack.peek()
|
||||
.merge(r)
|
||||
.ifPresent(ranges::add);
|
||||
}
|
||||
});
|
||||
return rangeStack;
|
||||
}
|
||||
|
||||
private IntRange(int low, int high) {
|
||||
this.low = low;
|
||||
this.high = high;
|
||||
}
|
||||
public int getLow() {
|
||||
return low;
|
||||
}
|
||||
public int getHigh() {
|
||||
return high;
|
||||
}
|
||||
/** Merge the other IntRange into this one, if it fits. */
|
||||
public Optional<IntRange> merge(IntRange other) {
|
||||
if (this.high == other.low) {
|
||||
this.high = other.high;
|
||||
return Optional.empty();
|
||||
} else if (this.low == other.high) {
|
||||
this.low = other.low;
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(other);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("[%d..%d)", low, high);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ import picocli.CommandLine.Option;
|
|||
commandListHeading = "%nCommands:%n",
|
||||
optionListHeading = "%nOptions:%n",
|
||||
description = "AppleSingle utility",
|
||||
subcommands = { HelpCommand.class, InfoCommand.class, CreateCommand.class, ExtractCommand.class })
|
||||
subcommands = { HelpCommand.class, InfoCommand.class, AnalyzeCommand.class, CreateCommand.class, ExtractCommand.class })
|
||||
public class Main implements Runnable {
|
||||
@Option(names = "--debug", description = "Dump full stack trackes if an error occurs")
|
||||
private static boolean debugFlag;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package io.github.applecommander.applesingle.tools.asu;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/** An OutputStream that doesn't do output. */
|
||||
public class NullOutputStream extends OutputStream {
|
||||
public static final NullOutputStream INSTANCE = new NullOutputStream();
|
||||
|
||||
private NullOutputStream() { /* Prevent construction */ }
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
// Do Nothing
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue