From 85a3e2a12a8d4c519c2a12e33bd896a4ffaf8790 Mon Sep 17 00:00:00 2001 From: Rob Greene Date: Fri, 30 Dec 2022 15:55:14 -0600 Subject: [PATCH] First draft of filter file command to apply a filter to an existing file. --- .../{ExportMethod.java => FilterMethod.java} | 4 +- .../io/github/applecommander/acx/Main.java | 2 + .../acx/command/ExportCommand.java | 22 +- .../acx/command/FilterCommand.java | 287 ++++++++++++++++++ ...verter.java => FilterMethodConverter.java} | 22 +- 5 files changed, 313 insertions(+), 24 deletions(-) rename app/cli-acx/src/main/java/io/github/applecommander/acx/{ExportMethod.java => FilterMethod.java} (97%) create mode 100644 app/cli-acx/src/main/java/io/github/applecommander/acx/command/FilterCommand.java rename app/cli-acx/src/main/java/io/github/applecommander/acx/converter/{ExportMethodConverter.java => FilterMethodConverter.java} (72%) diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/ExportMethod.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/FilterMethod.java similarity index 97% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/ExportMethod.java rename to app/cli-acx/src/main/java/io/github/applecommander/acx/FilterMethod.java index d324848..b158737 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/ExportMethod.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/FilterMethod.java @@ -42,7 +42,7 @@ import com.webcodepro.applecommander.storage.filters.TextFileFilter; import io.github.applecommander.filters.AppleSingleFileFilter; import io.github.applecommander.filters.RawFileFilter; -public enum ExportMethod { +public enum FilterMethod { APPLESINGLE(AppleSingleFileFilter::new, "as", "applesingle"), APPLESOFT(ApplesoftFileFilter::new, "bas", "applesoft"), APPLEWORKS_DATABASE(AppleWorksDataBaseFileFilter::new, "adb"), @@ -63,7 +63,7 @@ public enum ExportMethod { private Supplier constructor; private List codes; - private ExportMethod(Supplier constructor, String... codes) { + private FilterMethod(Supplier constructor, String... codes) { this.constructor = constructor; this.codes = Arrays.asList(codes); } diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java index 40ac3f4..ead13df 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java @@ -32,6 +32,7 @@ import io.github.applecommander.acx.command.DeleteCommand; import io.github.applecommander.acx.command.DiskMapCommand; import io.github.applecommander.acx.command.DumpCommand; import io.github.applecommander.acx.command.ExportCommand; +import io.github.applecommander.acx.command.FilterCommand; import io.github.applecommander.acx.command.FindDuplicateFilesCommand; import io.github.applecommander.acx.command.ImportCommand; import io.github.applecommander.acx.command.InfoCommand; @@ -66,6 +67,7 @@ import picocli.CommandLine.Option; DiskMapCommand.class, DumpCommand.class, ExportCommand.class, + FilterCommand.class, FindDuplicateFilesCommand.class, HelpCommand.class, ImportCommand.class, diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ExportCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ExportCommand.java index 5efa3da..3924459 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ExportCommand.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ExportCommand.java @@ -39,10 +39,10 @@ import com.webcodepro.applecommander.util.filestreamer.FileStreamer; import com.webcodepro.applecommander.util.filestreamer.FileTuple; import com.webcodepro.applecommander.util.filestreamer.TypeOfFile; -import io.github.applecommander.acx.ExportMethod; +import io.github.applecommander.acx.FilterMethod; import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions; -import io.github.applecommander.acx.converter.ExportMethodConverter; -import io.github.applecommander.acx.converter.ExportMethodConverter.ExportMethodCandidates; +import io.github.applecommander.acx.converter.FilterMethodConverter; +import io.github.applecommander.acx.converter.FilterMethodConverter.FilterMethodCandidates; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; @@ -144,21 +144,21 @@ public class ExportCommand extends ReadOnlyDiskImageCommandOptions { private static class FileExtractMethods { private Function extractFunction = this::asSuggestedFile; - @Option(names = { "--method" }, converter = ExportMethodConverter.class, - completionCandidates = ExportMethodCandidates.class, + @Option(names = { "--method" }, converter = FilterMethodConverter.class, + completionCandidates = FilterMethodCandidates.class, description = "Select a specific export method type (${COMPLETION-CANDIDATES}).") - public void selectExportMethod(final ExportMethod exportMethod) { - this.extractFunction = fileFilter -> exportMethod.create(); + public void selectFilterMethod(final FilterMethod filterMethod) { + this.extractFunction = fileFilter -> filterMethod.create(); } // Short-cuts to some of the more common, non-suggested, filters @Option(names = { "--raw", "--binary" }, description = "Extract file in native format.") public void setBinaryExtraction(boolean flag) { - selectExportMethod(ExportMethod.BINARY); + selectFilterMethod(FilterMethod.BINARY); } @Option(names = { "--hex", "--dump" }, description = "Extract file in hex dump format.") public void setHexDumpExtraction(boolean flag) { - selectExportMethod(ExportMethod.HEX_DUMP); + selectFilterMethod(FilterMethod.HEX_DUMP); } @Option(names = { "--suggested" }, description = "Extract file as suggested by AppleCommander (default)") public void setSuggestedExtraction(boolean flag) { @@ -166,11 +166,11 @@ public class ExportCommand extends ReadOnlyDiskImageCommandOptions { } @Option(names = { "--as", "--applesingle" }, description = "Extract file to AppleSingle file.") public void setAppleSingleExtraction(boolean flag) { - selectExportMethod(ExportMethod.APPLESINGLE); + selectFilterMethod(FilterMethod.APPLESINGLE); } @Option(names = { "--disassembly" }, description = "Dissassembly file.") public void setDisassemblyExtraction(boolean flag) { - selectExportMethod(ExportMethod.DISASSEMBLY); + selectFilterMethod(FilterMethod.DISASSEMBLY); } public FileFilter asSuggestedFile(FileEntry entry) { diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/FilterCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/FilterCommand.java new file mode 100644 index 0000000..8399e3e --- /dev/null +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/FilterCommand.java @@ -0,0 +1,287 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2019-2022 by Robert Greene and others + * robgreene at users.sourceforge.net + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package io.github.applecommander.acx.command; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.FileEntry; +import com.webcodepro.applecommander.storage.FileFilter; +import com.webcodepro.applecommander.storage.FormattedDisk; +import com.webcodepro.applecommander.util.readerwriter.FileEntryReader; +import com.webcodepro.applecommander.util.readerwriter.OverrideFileEntryReader; + +import io.github.applecommander.acx.FilterMethod; +import io.github.applecommander.acx.base.ReusableCommandOptions; +import io.github.applecommander.acx.converter.FilterMethodConverter; +import io.github.applecommander.acx.converter.FilterMethodConverter.FilterMethodCandidates; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Spec; + +@Command(name = "filter", description = "Filter on-disk file (that are not in a disk image).") +public class FilterCommand extends ReusableCommandOptions { + @Spec + private CommandSpec spec; + + @ArgGroup(exclusive = true, heading = "%nInput source:%n") + private InputData inputData = new InputData(); + + @ArgGroup(exclusive = true, heading = "%nFile filter methods:%n") + private FileFilterMethods extraction = new FileFilterMethods(); + + @ArgGroup(exclusive = true, heading = "%nOutput destination:%n") + private OutputData outputData = new OutputData(); + + @Override + public int handleCommand() throws Exception { + FileEntry fileEntry = inputData.asFileEntry(); + byte[] data = extraction.filter(fileEntry); + outputData.write(data); + + return 0; + } + + private static class InputData { + private Supplier fileEntryReaderSupplier = this::fromStdin; + + public FileEntry asFileEntry() { + return new FileEntryMimic(fileEntryReaderSupplier.get()); + } + + @Option(names = { "--stdin" }, description = "Read from standard input (default).") + public void stdinFlag(boolean flag) { + fileEntryReaderSupplier = this::fromStdin; + } + private FileEntryReader fromStdin() { + try { + byte[] data = System.in.readAllBytes(); + return OverrideFileEntryReader.builder() + .fileData(data) + .build(); + } catch (IOException cause) { + throw new UncheckedIOException(cause); + } + } + + @Option(names = { "--in", "--input" }, description = "File to read. (Required if specifying two file names.)") + public void inputFileFlag(final String filename) { + fromFileParameter(filename); + } + + @Parameters(description = "File to read.") + public void fromFileParameter(final String filename) { + fileEntryReaderSupplier = () -> { + try { + Path path = Path.of(filename); + byte[] data = Files.readAllBytes(path); + return OverrideFileEntryReader.builder() + .fileData(data) + .filename(filename) + .build(); + } catch (IOException cause) { + throw new UncheckedIOException(cause); + } + }; + } + } + + private static class FileFilterMethods { + private FilterMethod filterMethod = FilterMethod.TEXT; + public byte[] filter(FileEntry fileEntry) { + return filterMethod.create().filter(fileEntry); + } + + @Option(names = { "--method" }, converter = FilterMethodConverter.class, + completionCandidates = FilterMethodCandidates.class, + description = "Select a specific export method type (${COMPLETION-CANDIDATES}).") + public void selectFilterMethod(final FilterMethod filterMethod) { + this.filterMethod = filterMethod; + } + + // Short-cuts to some of the more common, non-suggested, filters + @Option(names = { "--text" }, description = "Treat file as Apple II text file (default).") + public void setTextFilter(boolean flag) { + selectFilterMethod(FilterMethod.TEXT); + } + @Option(names = { "--disassembly" }, description = "Disassemble input file.") + public void setDisassemblyFilter(boolean flag) { + selectFilterMethod(FilterMethod.DISASSEMBLY); + } + @Option(names = { "--applesoft" }, description = "De-tokenize Applesoft input file.") + public void setApplesoftFilter(boolean flag) { + selectFilterMethod(FilterMethod.DISASSEMBLY); + } + } + + private static class OutputData { + private Consumer outputDataConsumer = this::writeToStdout; + public void write(byte[] data) { + outputDataConsumer.accept(data); + } + + @Option(names = { "--stdout" }, description = "Write to standard output (default).") + public void stdoutFlag(boolean flag) { + outputDataConsumer = this::writeToStdout; + } + private void writeToStdout(byte[] data) { + try { + System.out.write(data); + } catch (IOException cause) { + throw new UncheckedIOException(cause); + } + } + + @Option(names = { "--out", "--output" }, description = "File to write. (Required if using stdin but writing to file.)") + public void outputFile(final String filename) { + toFile(filename); + } + + @Parameters(description = "File to write.") + public void toFile(final String filename) { + outputDataConsumer = (data) -> { + try { + Files.write(Path.of(filename), data); + } catch (IOException cause) { + throw new UncheckedIOException(cause); + } + }; + } + } + + public static class FileEntryMimic implements FileEntry { + private FileEntryReader fileEntryReader; + + public FileEntryMimic(FileEntryReader fileEntryReader) { + Objects.requireNonNull(fileEntryReader); + this.fileEntryReader = fileEntryReader; + } + + @Override + public String getFilename() { + return fileEntryReader.getFilename().orElse("UNKNOWN"); + } + + @Override + public void setFilename(String filename) { + throw new UnsupportedOperationException(); + } + + @Override + public String getFiletype() { + return fileEntryReader.getProdosFiletype().orElse("BIN"); + } + + @Override + public void setFiletype(String filetype) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLocked() { + return fileEntryReader.isLocked().orElse(false); + } + + @Override + public void setLocked(boolean lock) { + throw new UnsupportedOperationException(); + } + + @Override + public int getSize() { + return getFileData().length; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public boolean isDeleted() { + return false; + } + + @Override + public void delete() { + throw new UnsupportedOperationException(); + } + + @Override + public List getFileColumnData(int displayMode) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getFileData() { + return fileEntryReader.getFileData().orElse(new byte[0]); + } + + @Override + public void setFileData(byte[] data) throws DiskFullException { + throw new UnsupportedOperationException(); + } + + @Override + public FileFilter getSuggestedFilter() { + throw new UnsupportedOperationException(); + } + + @Override + public FormattedDisk getFormattedDisk() { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaximumFilenameLength() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean needsAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public void setAddress(int address) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAddress() { + return fileEntryReader.getBinaryAddress().orElse(0x800); + } + + @Override + public boolean canCompile() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/converter/ExportMethodConverter.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/converter/FilterMethodConverter.java similarity index 72% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/converter/ExportMethodConverter.java rename to app/cli-acx/src/main/java/io/github/applecommander/acx/converter/FilterMethodConverter.java index dba1667..b31cf02 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/converter/ExportMethodConverter.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/converter/FilterMethodConverter.java @@ -24,33 +24,33 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import io.github.applecommander.acx.ExportMethod; +import io.github.applecommander.acx.FilterMethod; import picocli.CommandLine.ITypeConverter; import picocli.CommandLine.TypeConversionException; -public class ExportMethodConverter implements ITypeConverter { - public static final Map EXPORTS = new HashMap<>(); +public class FilterMethodConverter implements ITypeConverter { + public static final Map FILTERS = new HashMap<>(); static { - for (ExportMethod x : ExportMethod.values()) { + for (FilterMethod x : FilterMethod.values()) { for (String code : x.getCodes()) { - EXPORTS.put(code, x); + FILTERS.put(code, x); } } } @Override - public ExportMethod convert(String value) throws Exception { - if (EXPORTS.containsKey(value)) { - return EXPORTS.get(value); + public FilterMethod convert(String value) throws Exception { + if (FILTERS.containsKey(value)) { + return FILTERS.get(value); } throw new TypeConversionException(String.format("Export method not found: %s", value)); } - public static class ExportMethodCandidates extends ArrayList { + public static class FilterMethodCandidates extends ArrayList { private static final long serialVersionUID = -744232190636905235L; - ExportMethodCandidates() { - super(EXPORTS.keySet()); + FilterMethodCandidates() { + super(FILTERS.keySet()); Collections.sort(this); } }