From 4627e08f9fbfa25cc76777c4461ec6de152504b2 Mon Sep 17 00:00:00 2001 From: Rob Greene Date: Sun, 3 Jun 2018 15:42:07 -0500 Subject: [PATCH] Adding Filter and altering AppleSingle write mechanism to be more reusable. --- .../applesingle/AppleSingle.java | 89 ++++------- .../applesingle/AppleSingleReader.java | 55 +++++-- .../applecommander/applesingle/Entry.java | 18 ++- .../applesingle/FileDatesInfo.java | 14 +- .../applesingle/ProdosFileInfo.java | 15 +- .../applesingle/tools/asu/AnalyzeCommand.java | 7 +- .../applesingle/tools/asu/FilterCommand.java | 140 ++++++++++++++++++ .../applesingle/tools/asu/Main.java | 9 +- 8 files changed, 267 insertions(+), 80 deletions(-) create mode 100644 tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/FilterCommand.java diff --git a/api/src/main/java/io/github/applecommander/applesingle/AppleSingle.java b/api/src/main/java/io/github/applecommander/applesingle/AppleSingle.java index b0f5c80..09628ac 100644 --- a/api/src/main/java/io/github/applecommander/applesingle/AppleSingle.java +++ b/api/src/main/java/io/github/applecommander/applesingle/AppleSingle.java @@ -27,6 +27,7 @@ import java.util.function.Consumer; * 1. Data Fork
* 2. Resource Fork
* 3. Real Name
+ * 8. File Dates Info
* 11. ProDOS File Info
* * @see AppleCommander issue #20 @@ -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 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 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 asEntries(byte[] data) throws IOException { Objects.requireNonNull(data); - return asEntries(AppleSingleReader.builder().data(data).build()); + return asEntries(AppleSingleReader.builder(data).build()); } public static List asEntries(AppleSingleReader reader) throws IOException { Objects.requireNonNull(reader); diff --git a/api/src/main/java/io/github/applecommander/applesingle/AppleSingleReader.java b/api/src/main/java/io/github/applecommander/applesingle/AppleSingleReader.java index f4ccb27..9c10f3f 100644 --- a/api/src/main/java/io/github/applecommander/applesingle/AppleSingleReader.java +++ b/api/src/main/java/io/github/applecommander/applesingle/AppleSingleReader.java @@ -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 versionReporter = v -> {}; private Consumer numberOfEntriesReporter = n -> {}; private Consumer 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 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 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 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); }; } } } \ No newline at end of file diff --git a/api/src/main/java/io/github/applecommander/applesingle/Entry.java b/api/src/main/java/io/github/applecommander/applesingle/Entry.java index 15c4f22..dd9ccfe 100644 --- a/api/src/main/java/io/github/applecommander/applesingle/Entry.java +++ b/api/src/main/java/io/github/applecommander/applesingle/Entry.java @@ -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); + } } diff --git a/api/src/main/java/io/github/applecommander/applesingle/FileDatesInfo.java b/api/src/main/java/io/github/applecommander/applesingle/FileDatesInfo.java index 8ce7f72..2f89473 100644 --- a/api/src/main/java/io/github/applecommander/applesingle/FileDatesInfo.java +++ b/api/src/main/java/io/github/applecommander/applesingle/FileDatesInfo.java @@ -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); } diff --git a/api/src/main/java/io/github/applecommander/applesingle/ProdosFileInfo.java b/api/src/main/java/io/github/applecommander/applesingle/ProdosFileInfo.java index 4620393..d0482e4 100644 --- a/api/src/main/java/io/github/applecommander/applesingle/ProdosFileInfo.java +++ b/api/src/main/java/io/github/applecommander/applesingle/ProdosFileInfo.java @@ -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.
*/ 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; } diff --git a/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/AnalyzeCommand.java b/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/AnalyzeCommand.java index 383ef10..5e6bebc 100644 --- a/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/AnalyzeCommand.java +++ b/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/AnalyzeCommand.java @@ -53,10 +53,9 @@ public class AnalyzeCommand implements Callable { List 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) diff --git a/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/FilterCommand.java b/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/FilterCommand.java new file mode 100644 index 0000000..b80a0d2 --- /dev/null +++ b/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/FilterCommand.java @@ -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 { + @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 included = toSet(includeEntryIds, osFilter); + SortedSet excluded = toSet(excludeEntryIds, null); + List entries = stdinFlag ? AppleSingle.asEntries(System.in) : AppleSingle.asEntries(inputFile); + List 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 before = toEntryType(entries); + SortedSet 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 toSet(Integer[] entryIds, OSFilter filter) { + SortedSet 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 toEntryType(Collection entries) { + return entries.stream() + .map(e -> e.getEntryId()) + .map(EntryType::find) + .collect(Collectors.toCollection(() -> new TreeSet())); + } + + 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; + } + } +} diff --git a/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/Main.java b/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/Main.java index ff3d1b0..d186f9d 100644 --- a/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/Main.java +++ b/tools/asu/src/main/java/io/github/applecommander/applesingle/tools/asu/Main.java @@ -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;