diff --git a/src/main/java/com/webcodepro/applecommander/ui/ac.java b/src/main/java/com/webcodepro/applecommander/ui/ac.java index c2540b6..9b82c5f 100644 --- a/src/main/java/com/webcodepro/applecommander/ui/ac.java +++ b/src/main/java/com/webcodepro/applecommander/ui/ac.java @@ -136,7 +136,9 @@ public class ac { putFile(args[1], new Name(args[2]), args[3], (args.length > 4 ? args[4] : "0x2000")); } else if ("-pt".equalsIgnoreCase(args[0])) { - putTxtFile(args[1], new Name(args[2])); + putTxtFileSetHighBit(args[1], new Name(args[2])); + } else if ("-ptx".equalsIgnoreCase(args[0])) { + putTxtFileClearHighBit(args[1], new Name(args[2])); } else if ("-d".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ deleteFile(args[1], args[2]); } else if ("-k".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ @@ -270,10 +272,20 @@ public class ac { * Put <stdin>. as an Apple text file into the file named * fileName on the disk named imageName. */ - static void putTxtFile(String imageName, Name name) throws IOException, DiskException { + static void putTxtFileSetHighBit(String imageName, Name name) throws IOException, DiskException { + // Order on the stream is important to ensure the translated newlines have the high bit done appropriately putFile(imageName, name, "TXT", "0", TranslatorStream.builder(System.in).lfToCr().setHighBit().get()); } + /** + * Put <stdin>. as an Apple text file into the file named + * fileName on the disk named imageName. + */ + static void putTxtFileClearHighBit(String imageName, Name name) throws IOException, DiskException { + // Order on the stream is important to ensure the translated newlines have the high bit done appropriately + putFile(imageName, name, "TXT", "0", TranslatorStream.builder(System.in).lfToCr().clearHighBit().get()); + } + /** * Put InputStream into the file named fileName on the disk named imageName; * Note: only volume level supported; input size unlimited. diff --git a/src/main/java/com/webcodepro/applecommander/util/TranslatorStream.java b/src/main/java/com/webcodepro/applecommander/util/TranslatorStream.java index 7e5d73a..0ae15d3 100644 --- a/src/main/java/com/webcodepro/applecommander/util/TranslatorStream.java +++ b/src/main/java/com/webcodepro/applecommander/util/TranslatorStream.java @@ -17,12 +17,19 @@ public class TranslatorStream extends InputStream { @Override public int read() throws IOException { - return fn.apply(sourceStream.read()); + int data = sourceStream.read(); + if (data == -1) { + return -1; + } + return fn.apply(data); } private int setHighBit(int value) { return value | 0x80; } + private int clearHighBit(int value) { + return value & 0x7f; + } private int lfToCr(int value) { if (value == '\r') { try { @@ -58,6 +65,9 @@ public class TranslatorStream extends InputStream { public Builder setHighBit() { return fn(stream::setHighBit); } + public Builder clearHighBit() { + return fn(stream::clearHighBit); + } public Builder lfToCr() { return fn(stream::lfToCr); } diff --git a/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties b/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties index 5e117a4..95733c9 100644 --- a/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties +++ b/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties @@ -103,29 +103,31 @@ CommandLineErrorMessage = Error: {0} CommandLineNoMatchMessage = {0}: No match. CommandLineStatus = {0} format; {1} bytes free; {2} bytes used. CommandLineHelp = \ - CommandLineHelp = AppleCommander command line options [{0}]:\n\ - -i [] display information about image(s).\n\ - -ls [] list brief directory of image(s).\n\ - -l [] list directory of image(s).\n\ - -ll [] list detailed directory of image(s).\n\ - -e [] export file from image to stdout\n or to an output file.\n\ - -x [] extract all files from image to directory.\n\ - -g [] get raw file from image to stdout\n or to an output file.\n\ - -p [[$|0x]] put stdin\n in filename on image, using file type and address [0x2000].\n\ - -pt put stdin in filename on image\n defaulting to TXT file type, setting high bit on and replacing\n newline characters with $8D.\n\ - -d delete file from image.\n\ - -k lock file on image.\n\ - -u unlock file on image.\n\ - -n change volume name (ProDOS or Pascal).\n\ - -dos put stdin with DOS header\n in filename on image, using file type and address from header.\n\ - -as [] put stdin with AppleSingle format\n in filename on image, using file type, address, and (optionally) name\n from the AppleSingle file.\n\ - -geos interpret stdin as a GEOS conversion file and\n place it on image (ProDOS only).\n\ - -dos140 create a 140K DOS 3.3 image.\n-pro140 create a 140K ProDOS image.\n\ - -pro800 create an 800K ProDOS image.\n\ - -pas140 create a 140K Pascal image.\n\ - -pas800 create an 800K Pascal image.\n\ + AppleCommander command line options [{0}]:\n\ + -i [] display information about image(s).\n\ + -ls [] list brief directory of image(s).\n\ + -l [] list directory of image(s).\n\ + -ll [] list detailed directory of image(s).\n\ + -e [] export file from image to stdout\n or to an output file.\n\ + -x [] extract all files from image to directory.\n\ + -g [] get raw file from image to stdout\n or to an output file.\n\ + -p [[$|0x]] put stdin\n in filename on image, using file type and address [0x2000].\n\ + -pt put stdin in filename on image\n defaulting to TXT file type, setting high bit on and replacing\n newline characters with $8D.\n\ + -ptx put stdin in filename on image\n defaulting to TXT file type, clearing high bit and replacing\n newline characters with $0D.\n\ + -d delete file from image.\n\ + -k lock file on image.\n\ + -u unlock file on image.\n\ + -n change volume name (ProDOS or Pascal).\n\ + -dos put stdin with DOS header\n in filename on image, using file type and address from header.\n\ + -as [] put stdin with AppleSingle format\n in filename on image, using file type, address, and (optionally) name\n from the AppleSingle file.\n\ + -geos interpret stdin as a GEOS conversion file and\n place it on image (ProDOS only).\n\ + -dos140 create a 140K DOS 3.3 image.\n\ + -pro140 \n create a 140K ProDOS image.\n\ + -pro800 create an 800K ProDOS image.\n\ + -pas140 create a 140K Pascal image.\n\ + -pas800 create an 800K Pascal image.\n\ -convert [] uncompress a ShrinkIt or Binary\n II file; or convert a DiskCopy 4.2 image into a ProDOS disk image.\n\ - -bas import an AppleSoft basic file from text\n back to it's tokenized format. + -bas import an AppleSoft basic file from text\n back to it's tokenized format. CommandLineSDKReadOnly = SDK, SHK, and DC42 files are read-only. Use the convert option on them first. CommandLineDC42Bad = Unable to interpret this DiskCopy 42 image. diff --git a/src/test/java/com/webcodepro/applecommander/util/TranslatorStreamTest.java b/src/test/java/com/webcodepro/applecommander/util/TranslatorStreamTest.java index 2b39532..47cac44 100644 --- a/src/test/java/com/webcodepro/applecommander/util/TranslatorStreamTest.java +++ b/src/test/java/com/webcodepro/applecommander/util/TranslatorStreamTest.java @@ -4,35 +4,37 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Supplier; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +@RunWith(Parameterized.class) public class TranslatorStreamTest { - @Test - public void testUnixLineEndings() throws IOException { - byte[] source = "Hello\nWorld!\n".getBytes(); - testAgainstExpected(source); + @Parameters(name = "{index}: {0}") + public static Collection data() { + return Arrays.asList( + TestData.builder().unixLineEndings().highBitSet().build(), + TestData.builder().dosLineEndings().highBitSet().build(), + TestData.builder().appleLineEndings().highBitSet().build(), + TestData.builder().unixLineEndings().highBitClear().build(), + TestData.builder().dosLineEndings().highBitClear().build(), + TestData.builder().appleLineEndings().highBitClear().build() + ); } + + @Parameter + public TestData testData; @Test - public void testDosLineEndings() throws IOException { - byte[] source = "Hello\r\nWorld!\r\n".getBytes(); - testAgainstExpected(source); - } - - @Test - public void testAppleLineEndings() throws IOException { - byte[] source = "Hello\rWorld!\r".getBytes(); - testAgainstExpected(source); - } - - private void testAgainstExpected(final byte[] source) throws IOException { - final byte[] expected = { (byte)0xc8, (byte)0xe5, (byte)0xec, (byte)0xec, - (byte)0xef, (byte)0x8d, (byte)0xd7, (byte)0xef, (byte)0xf2, - (byte)0xec, (byte)0xe4, (byte)0xa1, (byte)0x8d }; - - InputStream is = TranslatorStream.builder(new ByteArrayInputStream(source)).lfToCr().setHighBit().get(); + public void test() throws IOException { + InputStream is = testData.getSourceStream(); byte[] actual = null; try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { @@ -40,6 +42,93 @@ public class TranslatorStreamTest { actual = os.toByteArray(); } - Assert.assertArrayEquals(expected, actual); + Assert.assertArrayEquals(testData.getExpected(), actual); + } + + public static class TestData { + private String name; + private byte[] expected; + private InputStream sourceStream; + + public TestData(String name, byte[] expected, InputStream sourceStream) { + this.name = name; + this.expected = expected; + this.sourceStream = sourceStream; + } + public byte[] getExpected() { + return expected; + } + public InputStream getSourceStream() { + return sourceStream; + } + @Override + public String toString() { + return name; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String expectedDescription; + private byte[] expected; + private String sourceDescription; + private byte[] source; + private Supplier configurer; + + public Builder unixLineEndings() { + this.sourceDescription = "unix line endings"; + this.source = "Hello\nWorld!\n".getBytes(); + return this; + } + public Builder dosLineEndings() { + this.sourceDescription = "dos line endings"; + this.source = "Hello\r\nWorld!\r\n".getBytes(); + return this; + } + public Builder appleLineEndings() { + this.sourceDescription = "apple line endings"; + this.source = "Hello\rWorld!\r".getBytes(); + return this; + } + /** "Hello^MWorld!^M" with high bit set. */ + public Builder highBitSet() { + this.expectedDescription = "high bit set"; + this.expected = new byte[] { + (byte)0xc8, (byte)0xe5, (byte)0xec, (byte)0xec, + (byte)0xef, (byte)0x8d, (byte)0xd7, (byte)0xef, (byte)0xf2, + (byte)0xec, (byte)0xe4, (byte)0xa1, (byte)0x8d + }; + this.configurer = this::highBitSetConfigurer; + return this; + } + private TranslatorStream highBitSetConfigurer() { + return TranslatorStream.builder(new ByteArrayInputStream(source)).lfToCr().setHighBit().get(); + } + /** "Hello^MWorld!^M" with high bit clear. */ + public Builder highBitClear() { + this.expectedDescription = "high bit clear"; + this.expected = new byte[] { + 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x0d, 0x57, 0x6f, 0x72, + 0x6c, 0x64, 0x21, 0x0d + }; + this.configurer = this::highBitClearConfigurer; + return this; + } + private TranslatorStream highBitClearConfigurer() { + return TranslatorStream.builder(new ByteArrayInputStream(source)).lfToCr().clearHighBit().get(); + } + public TestData build() { + if (this.expected == null || this.expectedDescription == null + || this.source == null || this.sourceDescription == null + || this.configurer == null) { + throw new RuntimeException("Not all variables are set!"); + } + String name = String.format("%s with %s", this.sourceDescription, this.expectedDescription); + return new TestData(name, this.expected, this.configurer.get()); + } + } } }