Adding Filter and altering AppleSingle write mechanism to be more

reusable.
This commit is contained in:
Rob Greene 2018-06-03 15:42:07 -05:00
parent 4f34a8a289
commit 4627e08f9f
8 changed files with 267 additions and 80 deletions

View File

@ -27,6 +27,7 @@ import java.util.function.Consumer;
* 1. Data Fork<br/>
* 2. Resource Fork<br/>
* 3. Real Name<br/>
* 8. File Dates Info<br/>
* 11. ProDOS File Info<br/>
*
* @see <a href="https://github.com/AppleCommander/AppleCommander/issues/20">AppleCommander issue #20</a>
@ -80,28 +81,24 @@ public class AppleSingle {
}
public void save(OutputStream outputStream) throws IOException {
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 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);
List<Entry> entries = new ArrayList<>();
Optional.ofNullable(this.realName)
.map(String::getBytes)
.map(b -> Entry.create(EntryType.REAL_NAME, b))
.ifPresent(entries::add);
Optional.ofNullable(this.prodosFileInfo)
.map(ProdosFileInfo::toEntry)
.ifPresent(entries::add);
Optional.ofNullable(this.fileDatesInfo)
.map(FileDatesInfo::toEntry)
.ifPresent(entries::add);
Optional.ofNullable(this.resourceFork)
.map(b -> Entry.create(EntryType.RESOURCE_FORK, b))
.ifPresent(entries::add);
Optional.ofNullable(this.dataFork)
.map(b -> Entry.create(EntryType.DATA_FORK, b))
.ifPresent(entries::add);
write(outputStream, entries);
}
public void save(File file) throws IOException {
try (FileOutputStream outputStream = new FileOutputStream(file)) {
@ -114,46 +111,24 @@ public class AppleSingle {
}
}
private void writeFileHeader(OutputStream outputStream, int numberOfEntries) throws IOException {
public static void write(OutputStream outputStream, List<Entry> entries) throws IOException {
final byte[] filler = new byte[16];
ByteBuffer buf = ByteBuffer.allocate(26).order(ByteOrder.BIG_ENDIAN);
buf.putInt(MAGIC_NUMBER);
buf.putInt(VERSION_NUMBER2);
buf.put(filler);
buf.putShort((short)numberOfEntries);
buf.putShort((short)entries.size());
outputStream.write(buf.array());
}
private void writeHeader(OutputStream outputStream, int entryId, int offset, int length) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(12).order(ByteOrder.BIG_ENDIAN);
buf.putInt(entryId);
buf.putInt(offset);
buf.putInt(length);
outputStream.write(buf.array());
}
private void writeRealName(OutputStream outputStream) throws IOException {
outputStream.write(realName.getBytes());
}
private void writeProdosFileInfo(OutputStream outputStream) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
buf.putShort((short)prodosFileInfo.access);
buf.putShort((short)prodosFileInfo.fileType);
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);
}
private void writeDataFork(OutputStream outputStream) throws IOException {
outputStream.write(dataFork);
}
int offset = 26 + (Entry.BYTES * entries.size());
for (Entry entry : entries) {
entry.writeHeader(outputStream, offset);
offset += entry.getLength();
}
for (Entry entry : entries) {
entry.writeData(outputStream);
}
}
public static AppleSingle read(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream, "Please supply an input stream");
@ -186,7 +161,7 @@ public class AppleSingle {
}
public static List<Entry> asEntries(byte[] data) throws IOException {
Objects.requireNonNull(data);
return asEntries(AppleSingleReader.builder().data(data).build());
return asEntries(AppleSingleReader.builder(data).build());
}
public static List<Entry> asEntries(AppleSingleReader reader) throws IOException {
Objects.requireNonNull(reader);

View File

@ -5,13 +5,20 @@ import java.nio.ByteOrder;
import java.util.Objects;
import java.util.function.Consumer;
/**
* The AppleSingleReader is a component that allows tools to react to processing that
* goes on when an AppleSingle file is being read. The {@code Builder} allows multiple
* {@code Consumer}'s and {@code ReadAtReporter}'s to be defined.
*/
public final class AppleSingleReader {
private AppleSingleReader() { /* Prevent construction */ }
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) -> {};
private ReadAtReporter readAtReporter = (s,b,d) -> {};
public ByteBuffer read(int len, String description) {
try {
@ -23,7 +30,7 @@ public final class AppleSingleReader {
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);
readAtReporter.accept(start, chunk, description);
ByteBuffer buffer = ByteBuffer.wrap(chunk)
.order(ByteOrder.BIG_ENDIAN);
return buffer;
@ -38,41 +45,41 @@ public final class AppleSingleReader {
entryReporter.accept(entry);
}
public static Builder builder() {
return new Builder();
/** Create a {@code Builder} for an {@code AppleSingleReader}. */
public static Builder builder(byte[] data) {
return new Builder(data);
}
public static class Builder {
private AppleSingleReader reader = new AppleSingleReader();
private Builder() {
// Prevent construction
}
public Builder data(byte[] data) {
Objects.requireNonNull(data);
private Builder(byte[] data) {
Objects.requireNonNull(data, "You must supply a byte[] of data");
reader.data = data;
return this;
}
/** Add a version reporter. Note that multiple can be added. */
public Builder versionReporter(Consumer<Integer> consumer) {
Objects.requireNonNull(consumer);
reader.versionReporter = reader.versionReporter.andThen(consumer);
return this;
}
/** Add a number of entries reporter. Note that multiple can be added. */
public Builder numberOfEntriesReporter(Consumer<Integer> consumer) {
Objects.requireNonNull(consumer);
reader.numberOfEntriesReporter = reader.numberOfEntriesReporter.andThen(consumer);
return this;
}
/** Add an entry reporter. Note that multiple can be added. */
public Builder entryReporter(Consumer<Entry> consumer) {
Objects.requireNonNull(consumer);
reader.entryReporter = reader.entryReporter.andThen(consumer);
return this;
}
/** Add a read at reporter. Note that multiple can be added. */
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;
}
}
@ -82,10 +89,30 @@ public final class AppleSingleReader {
* 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) {
/**
* Performs this operation on the given arguments.
*
* @param start the offset into the file
* @param data the specific data being processed
* @param description descriptive text regarding the data
*/
public void accept(int start, byte[] data, String description);
/**
* Returns a composed {@code ReadAtReporter} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code ReadAtReporter} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
public default ReadAtReporter andThen(ReadAtReporter after) {
Objects.requireNonNull(after);
return (s,l,b,d) -> { accept(s,l,b,d); after.accept(s,l,b,d); };
return (s,b,d) -> { accept(s,b,d); after.accept(s,b,d); };
}
}
}

View File

@ -1,11 +1,13 @@
package io.github.applecommander.applesingle;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
public class Entry {
public static final int LENGTH = 12;
public static final int BYTES = 12;
private int entryId;
private int offset;
private int length;
@ -14,7 +16,7 @@ public class Entry {
public static Entry create(AppleSingleReader reader) {
Objects.requireNonNull(reader);
ByteBuffer buffer = reader.read(LENGTH, "Entry header");
ByteBuffer buffer = reader.read(BYTES, "Entry header");
Entry entry = new Entry();
entry.entryId = buffer.getInt();
entry.offset = buffer.getInt();
@ -49,4 +51,16 @@ public class Entry {
public ByteBuffer getBuffer() {
return ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).asReadOnlyBuffer();
}
public void writeHeader(OutputStream outputStream, int offset) throws IOException {
this.offset = offset;
ByteBuffer buf = ByteBuffer.allocate(BYTES).order(ByteOrder.BIG_ENDIAN);
buf.putInt(this.entryId);
buf.putInt(this.offset);
buf.putInt(this.length);
outputStream.write(buf.array());
}
public void writeData(OutputStream outputStream) throws IOException {
outputStream.write(data);
}
}

View File

@ -1,16 +1,19 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
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;
/** Number of bytes a File Dates Info takes per AppleSingle spec. */
public static final int BYTES = 16;
// Package scoped so AppleSingle Builder is able to set
int creation;
int modification;
int backup;
@ -42,6 +45,15 @@ public class FileDatesInfo {
this.access = access;
}
public Entry toEntry() {
ByteBuffer buf = ByteBuffer.allocate(BYTES).order(ByteOrder.BIG_ENDIAN);
buf.putInt(creation);
buf.putInt(modification);
buf.putInt(backup);
buf.putInt(access);
return Entry.create(EntryType.FILE_DATES_INFO, buf.array());
}
public Instant getCreationInstant() {
return toInstant(this::getCreation);
}

View File

@ -1,6 +1,7 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import io.github.applecommander.applesingle.AppleSingle.Builder;
@ -11,6 +12,10 @@ import io.github.applecommander.applesingle.AppleSingle.Builder;
* Note 2: Fields are package-private to allow {@link Builder} to have direct access.<br/>
*/
public class ProdosFileInfo {
/** Number of bytes a File Dates Info takes per AppleSingle spec. */
public static final int BYTES = 8;
// Package scoped so AppleSingle Builder is able to set
int access;
int fileType;
int auxType;
@ -31,7 +36,15 @@ public class ProdosFileInfo {
this.fileType = fileType;
this.auxType = auxType;
}
public Entry toEntry() {
ByteBuffer buf = ByteBuffer.allocate(BYTES).order(ByteOrder.BIG_ENDIAN);
buf.putShort((short)access);
buf.putShort((short)fileType);
buf.putInt(auxType);
return Entry.create(EntryType.PRODOS_FILE_INFO, buf.array());
}
public int getAccess() {
return access;
}

View File

@ -53,10 +53,9 @@ public class AnalyzeCommand implements Callable<Void> {
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))
AppleSingleReader reader = AppleSingleReader.builder(fileData)
.readAtReporter((start,b,d) -> used.add(IntRange.of(start, start + b.length)))
.readAtReporter((start,chunk,desc) -> dumper.dump(start, chunk, desc))
.versionReporter(this::reportVersion)
.numberOfEntriesReporter(this::reportNumberOfEntries)
.entryReporter(this::reportEntry)

View File

@ -0,0 +1,140 @@
package io.github.applecommander.applesingle.tools.asu;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.Entry;
import io.github.applecommander.applesingle.EntryType;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
/**
* Allow filtering of an an AppleSingle archive.
* Both source and target can be a file or stream.
*/
@Command(name = "filter", description = { "Filter 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 FilterCommand 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 = "--stdout", description = "Write AppleSingle to stdout.")
private boolean stdoutFlag;
@Option(names = { "-o", "--output" }, description = "Write AppleSingle to file.")
private Path outputFile;
@Parameters(arity = "0..1", description = "File to process")
private Path inputFile;
@Option(names = "--prodos", description = "Apply ProDOS specific filter")
private boolean prodosFlag;
@Option(names = { "--mac", "--macintosh" }, description = "Apply Macintosh specific filter")
private boolean macintoshFlag;
@Option(names = "--msdos", description = "Apply MS-DOS specific filter")
private boolean msdosFlag;
@Option(names = "--afp", description = "Apply AFP specific filter")
private boolean afpFlag;
@Option(names = "--include", description = "Filter by including specific entryIds", split = ",")
private Integer[] includeEntryIds;
@Option(names = "--exclude", description = "Filter by excluding specific entryIds", split = ",")
private Integer[] excludeEntryIds;
@Override
public Void call() throws IOException {
try (PrintStream ps = this.stdoutFlag ? new PrintStream(NullOutputStream.INSTANCE) : System.out) {
OSFilter osFilter = validate();
SortedSet<Integer> included = toSet(includeEntryIds, osFilter);
SortedSet<Integer> excluded = toSet(excludeEntryIds, null);
List<Entry> entries = stdinFlag ? AppleSingle.asEntries(System.in) : AppleSingle.asEntries(inputFile);
List<Entry> newEntries = entries.stream()
.filter(e -> included.isEmpty() || included.contains(e.getEntryId()))
.filter(e -> excluded.isEmpty() || !excluded.contains(e.getEntryId()))
.collect(Collectors.toList());
// Check if we ended up with different things
SortedSet<EntryType> before = toEntryType(entries);
SortedSet<EntryType> after = toEntryType(newEntries);
before.removeAll(after); // Note: modifies before
if (!before.isEmpty()) {
ps.printf("Removed the following entries:\n");
before.forEach(e -> ps.printf("- %s\n", e.name));
} else {
ps.printf("No entries removed.\n");
}
OutputStream outputStream = stdoutFlag ? System.out : Files.newOutputStream(outputFile);
AppleSingle.write(outputStream, newEntries);
}
return null;
}
private OSFilter validate() throws IOException {
long count = Stream.of(prodosFlag, macintoshFlag, msdosFlag, afpFlag).filter(flag -> flag).count();
// Expected boundaries
if (count == 0) return null;
if (count > 1) throw new IOException("Please choose only one operating system flag!");
// Set the correct OS Flag
if (prodosFlag) return OSFilter.PRODOS;
if (macintoshFlag) return OSFilter.MACINTOSH;
if (msdosFlag) return OSFilter.MS_DOS;
if (afpFlag) return OSFilter.AFP;
// Not a clue how you can get here...
throw new IOException("Bug! Please put in a ticket or a pull request. Thanks! :-)");
}
private SortedSet<Integer> toSet(Integer[] entryIds, OSFilter filter) {
SortedSet<Integer> set = new TreeSet<>();
Optional.ofNullable(entryIds)
.map(a -> Arrays.asList(a))
.ifPresent(set::addAll);
Optional.ofNullable(filter)
.map(f -> f.types)
.ifPresent(t -> Stream.of(t)
.map(e -> e.entryId)
.collect(Collectors.toCollection(() -> set)));
return set;
}
private SortedSet<EntryType> toEntryType(Collection<Entry> entries) {
return entries.stream()
.map(e -> e.getEntryId())
.map(EntryType::find)
.collect(Collectors.toCollection(() -> new TreeSet<EntryType>()));
}
public enum OSFilter {
PRODOS(EntryType.DATA_FORK, EntryType.RESOURCE_FORK, EntryType.REAL_NAME, EntryType.FILE_DATES_INFO,
EntryType.PRODOS_FILE_INFO),
MACINTOSH(EntryType.DATA_FORK, EntryType.RESOURCE_FORK, EntryType.REAL_NAME, EntryType.COMMENT,
EntryType.ICON_BW, EntryType.ICON_COLOR, EntryType.FILE_DATES_INFO, EntryType.FINDER_INFO,
EntryType.MACINTOSH_FILE_INFO),
MS_DOS(EntryType.DATA_FORK, EntryType.REAL_NAME, EntryType.FILE_DATES_INFO, EntryType.MSDOS_FILE_INFO),
AFP(EntryType.DATA_FORK, EntryType.REAL_NAME, EntryType.FILE_DATES_INFO, EntryType.SHORT_NAME,
EntryType.AFP_FILE_INFO, EntryType.DIRECTORY_ID);
public final EntryType[] types;
private OSFilter(EntryType... types) {
this.types = types;
}
}
}

View File

@ -15,7 +15,14 @@ import picocli.CommandLine.Option;
commandListHeading = "%nCommands:%n",
optionListHeading = "%nOptions:%n",
description = "AppleSingle utility",
subcommands = { HelpCommand.class, InfoCommand.class, AnalyzeCommand.class, CreateCommand.class, ExtractCommand.class })
subcommands = {
AnalyzeCommand.class,
CreateCommand.class,
ExtractCommand.class,
FilterCommand.class,
HelpCommand.class,
InfoCommand.class,
})
public class Main implements Runnable {
@Option(names = "--debug", description = "Dump full stack trackes if an error occurs")
private static boolean debugFlag;