diff --git a/src/main/java/com/webcodepro/applecommander/ui/ac.java b/src/main/java/com/webcodepro/applecommander/ui/ac.java index 6ac63d6..33e8f8c 100644 --- a/src/main/java/com/webcodepro/applecommander/ui/ac.java +++ b/src/main/java/com/webcodepro/applecommander/ui/ac.java @@ -53,6 +53,7 @@ import com.webcodepro.applecommander.storage.physical.ProdosOrder; import com.webcodepro.applecommander.util.AppleUtil; import com.webcodepro.applecommander.util.StreamUtil; import com.webcodepro.applecommander.util.TextBundle; +import com.webcodepro.applecommander.util.TranslatorStream; import io.github.applecommander.applesingle.AppleSingle; import io.github.applecommander.applesingle.ProdosFileInfo; @@ -134,6 +135,8 @@ public class ac { } else if ("-p".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ 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])); } else if ("-d".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ deleteFile(args[1], args[2]); } else if ("-k".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ @@ -260,7 +263,15 @@ public class ac { putFile(imageName, name, fileType, address, System.in); } - + + /** + * 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 { + putFile(imageName, name, "TXT", "0", TranslatorStream.builder(System.in).lfToCr().setHighBit().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 new file mode 100644 index 0000000..7e5d73a --- /dev/null +++ b/src/main/java/com/webcodepro/applecommander/util/TranslatorStream.java @@ -0,0 +1,69 @@ +package com.webcodepro.applecommander.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.io.UncheckedIOException; +import java.util.function.Function; + +public class TranslatorStream extends InputStream { + private PushbackInputStream sourceStream; + // Defaults to a no-op transformation + private Function fn = i -> i; + + private TranslatorStream(InputStream sourceStream) { + this.sourceStream = new PushbackInputStream(sourceStream); + } + + @Override + public int read() throws IOException { + return fn.apply(sourceStream.read()); + } + + private int setHighBit(int value) { + return value | 0x80; + } + private int lfToCr(int value) { + if (value == '\r') { + try { + int nextValue = sourceStream.read(); + if (nextValue == '\n') { + return '\r'; + } else { + if (nextValue != -1) sourceStream.unread(nextValue); + return value; + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + return (value == '\n') ? '\r' : value; + } + + public static Builder builder(InputStream sourceStream) { + return new Builder(sourceStream); + } + + public static class Builder { + private TranslatorStream stream; + + private Builder(InputStream sourceStream) { + stream = new TranslatorStream(sourceStream); + } + private Builder fn(Function andThen) { + stream.fn = stream.fn.andThen(andThen); + return this; + } + + public Builder setHighBit() { + return fn(stream::setHighBit); + } + public Builder lfToCr() { + return fn(stream::lfToCr); + } + + public TranslatorStream get() { + return stream; + } + } +} diff --git a/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties b/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties index fda64c0..5e117a4 100644 --- a/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties +++ b/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties @@ -102,7 +102,30 @@ CreateDirectoryMenuItem=Create Directory... 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-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-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. +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\ + -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. 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 new file mode 100644 index 0000000..2b39532 --- /dev/null +++ b/src/test/java/com/webcodepro/applecommander/util/TranslatorStreamTest.java @@ -0,0 +1,45 @@ +package com.webcodepro.applecommander.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Assert; +import org.junit.Test; + +public class TranslatorStreamTest { + @Test + public void testUnixLineEndings() throws IOException { + byte[] source = "Hello\nWorld!\n".getBytes(); + testAgainstExpected(source); + } + + @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(); + + byte[] actual = null; + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + StreamUtil.copy(is, os); + actual = os.toByteArray(); + } + + Assert.assertArrayEquals(expected, actual); + } +}