diff --git a/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java b/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java index 973882a..03cbbb6 100644 --- a/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java +++ b/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java @@ -68,8 +68,7 @@ public class ProdosFormatDisk extends FormattedDisk { * A complete list of all known ProDOS filetypes. Note that this * list really cannot be complete, as there are multiple mappings per * identifier in some cases - differentiated by AUXTYPE. This is - * loaded via initialize when the first instance of ProdosFormatDisk - * is created. + * loaded via the static initializer. */ private static ProdosFileType[] fileTypes; /** @@ -85,7 +84,7 @@ public class ProdosFormatDisk extends FormattedDisk { /** * This class holds filetype mappings. */ - private class ProdosFileType { + private static class ProdosFileType { private byte type; private String string; private boolean addressRequired; @@ -148,18 +147,15 @@ public class ProdosFormatDisk extends FormattedDisk { public ProdosFormatDisk(String filename, ImageOrder imageOrder) { super(filename, imageOrder); volumeHeader = new ProdosVolumeDirectoryHeader(this); - initialize(); } /** * Initialize all file types. */ - protected void initialize() { - if (fileTypes != null) return; - + static { fileTypes = new ProdosFileType[256]; InputStream inputStream = - getClass().getResourceAsStream("ProdosFileTypes.properties"); //$NON-NLS-1$ + ProdosFormatDisk.class.getResourceAsStream("ProdosFileTypes.properties"); //$NON-NLS-1$ Properties properties = new Properties(); try { properties.load(inputStream); @@ -1251,7 +1247,7 @@ public class ProdosFormatDisk extends FormattedDisk { * Return the filetype of this file. This will be three characters, * according to ProDOS - a "$xx" if unknown. */ - public String getFiletype(int filetype) { + public static String getFiletype(int filetype) { ProdosFileType prodostype = fileTypes[filetype]; return prodostype.getString(); } @@ -1270,7 +1266,7 @@ public class ProdosFormatDisk extends FormattedDisk { /** * Locate the associated ProdosFileType. */ - public ProdosFileType findFileType(String filetype) { + public static ProdosFileType findFileType(String filetype) { for (int i=0; i= 3 ? args[2] : null); } else if ("-geos".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ putGEOS(args[1]); } else if ("-dos140".equalsIgnoreCase(args[0])) { //$NON-NLS-1$ @@ -167,22 +175,23 @@ public class ac { ByteArrayOutputStream buf = new ByteArrayOutputStream(); byte[] inb = new byte[1024]; int byteCount = 0; - InputStream is = new FileInputStream(file); - while ((byteCount = is.read(inb)) > 0) { - buf.write(inb, 0, byteCount); - } - Disk disk = new Disk(imageName); - FormattedDisk[] formattedDisks = disk.getFormattedDisks(); - FormattedDisk formattedDisk = formattedDisks[0]; - FileEntry entry = name.createEntry(formattedDisk); - if (entry != null) { - entry.setFiletype(fileType); - entry.setFilename(name.name); - entry.setFileData(buf.toByteArray()); - if (entry.needsAddress()) { - entry.setAddress(stringToInt(address)); + try (InputStream is = new FileInputStream(file)) { + while ((byteCount = is.read(inb)) > 0) { + buf.write(inb, 0, byteCount); + } + Disk disk = new Disk(imageName); + FormattedDisk[] formattedDisks = disk.getFormattedDisks(); + FormattedDisk formattedDisk = formattedDisks[0]; + FileEntry entry = name.createEntry(formattedDisk); + if (entry != null) { + entry.setFiletype(fileType); + entry.setFilename(name.name); + entry.setFileData(buf.toByteArray()); + if (entry.needsAddress()) { + entry.setAddress(stringToInt(address)); + } + formattedDisk.save(); } - formattedDisk.save(); } } @@ -193,12 +202,18 @@ public class ac { static void putFile(String imageName, Name name, String fileType, String address) throws IOException, DiskException { + putFile(imageName, name, fileType, address, System.in); + } + + /** + * Put InputStream into the file named fileName on the disk named imageName; + * Note: only volume level supported; input size unlimited. + */ + static void putFile(String imageName, Name name, String fileType, + String address, InputStream inputStream) throws IOException, DiskException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); - byte[] inb = new byte[1024]; - int byteCount = 0; - while ((byteCount = System.in.read(inb)) > 0) { - buf.write(inb, 0, byteCount); - } + StreamUtil.copy(inputStream, buf); Disk disk = new Disk(imageName); FormattedDisk[] formattedDisks = disk.getFormattedDisks(); if (formattedDisks == null) @@ -250,6 +265,38 @@ public class ac { putFile(imageName, name, fileType, Integer.toString(address)); } } + + /** + * Put file from AppleSingle format into ProDOS image. + */ + public static void putAppleSingle(String imageName, String fileName) throws IOException, DiskException { + putAppleSingle(imageName, fileName, System.in); + } + /** + * AppleSingle shim to allow for unit testing. + */ + public static void putAppleSingle(String imageName, String fileName, InputStream inputStream) + throws IOException, DiskException { + + AppleSingle as = new AppleSingle(inputStream); + if (fileName == null) { + fileName = as.getRealName(); + } + if (fileName == null) { + throw new IOException("Please specify a file name - this AppleSingle does not have one."); + } + if (as.getProdosFileInfo() == null) { + throw new IOException("This AppleSingle does not contain a ProDOS file."); + } + if (as.getDataFork() == null || as.getDataFork().length == 0) { + throw new IOException("This AppleSingle does not contain a data fork."); + } + Name name = new Name(fileName); + ProdosFileInfo info = as.getProdosFileInfo(); + String fileType = ProdosFormatDisk.getFiletype(info.getFileType()); + putFile(imageName, name, fileType, Integer.toString(info.getAuxType()), + new ByteArrayInputStream(as.getDataFork())); + } /** * Interpret <stdin> as a GEOS file and place it on the disk named imageName. @@ -339,10 +386,8 @@ public class ac { /** * Recursive routine to write directory and file entries. */ - static void writeFiles(List files, String directory) throws IOException, DiskException { - Iterator it = files.iterator(); - while (it.hasNext()) { - FileEntry entry = (FileEntry) it.next(); + static void writeFiles(List files, String directory) throws IOException, DiskException { + for (FileEntry entry : files) { if ((entry != null) && (!entry.isDeleted()) && (!entry.isDirectory())) { FileFilter ff = entry.getSuggestedFilter(); if (ff instanceof BinaryFileFilter) @@ -367,11 +412,9 @@ public class ac { * file with the given filename. * @deprecated */ - static FileEntry getEntry(List files, String fileName) throws DiskException { - FileEntry entry = null; + static FileEntry getEntry(List files, String fileName) throws DiskException { if (files != null) { - for (int i = 0; i < files.size(); i++) { - entry = (FileEntry) files.get(i); + for (FileEntry entry : files) { String entryName = entry.getFilename(); if (!entry.isDeleted() && fileName.equalsIgnoreCase(entryName)) { return entry; @@ -399,7 +442,7 @@ public class ac { FormattedDisk formattedDisk = formattedDisks[i]; System.out.print(args[d] + " "); System.out.println(formattedDisk.getDiskName()); - List files = formattedDisk.getFiles(); + List files = formattedDisk.getFiles(); if (files != null) { showFiles(files, "", display); //$NON-NLS-1$ } @@ -423,11 +466,10 @@ public class ac { * system with directories (e.g. ProDOS), this really returns the first file * with the given filename. */ - static void showFiles(List files, String indent, int display) throws DiskException { - for (int i = 0; i < files.size(); i++) { - FileEntry entry = (FileEntry) files.get(i); + static void showFiles(List files, String indent, int display) throws DiskException { + for (FileEntry entry : files) { if (!entry.isDeleted()) { - List data = entry.getFileColumnData(display); + List data = entry.getFileColumnData(display); System.out.print(indent); for (int d = 0; d < data.size(); d++) { System.out.print(data.get(d)); @@ -451,9 +493,7 @@ public class ac { FormattedDisk[] formattedDisks = disk.getFormattedDisks(); for (int i = 0; i < formattedDisks.length; i++) { FormattedDisk formattedDisk = formattedDisks[i]; - Iterator iterator = formattedDisk.getDiskInformation().iterator(); - while (iterator.hasNext()) { - DiskInformation diskinfo = (DiskInformation) iterator.next(); + for (DiskInformation diskinfo : formattedDisk.getDiskInformation()) { System.out.println(diskinfo.getLabel() + ": " + diskinfo.getValue()); } } @@ -607,7 +647,7 @@ public class ac { } public FileEntry getEntry(FormattedDisk formattedDisk) throws DiskException { - List files = formattedDisk.getFiles(); + List files = formattedDisk.getFiles(); FileEntry entry = null; for (int i = 0; i < path.length - 1; i++) { String dirName = path[i]; @@ -633,7 +673,7 @@ public class ac { if (path.length == 1) { return formattedDisk.createFile(); } - List files = formattedDisk.getFiles(); + List files = formattedDisk.getFiles(); DirectoryEntry dir = null, parentDir = null; for (int i = 0; i < path.length - 1; i++) { String dirName = path[i]; diff --git a/src/main/java/com/webcodepro/applecommander/util/AppleSingle.java b/src/main/java/com/webcodepro/applecommander/util/AppleSingle.java new file mode 100644 index 0000000..3246ca8 --- /dev/null +++ b/src/main/java/com/webcodepro/applecommander/util/AppleSingle.java @@ -0,0 +1,112 @@ +package com.webcodepro.applecommander.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Support reading of data from and AppleSingle source. + * Does not implement all components at this time, extend as required. + * + * @see https://github.com/AppleCommander/AppleCommander/issues/20 + */ +public class AppleSingle { + public static final int MAGIC_NUMBER = 0x0051600; + public static final int VERSION_NUMBER = 0x00020000; + + private byte[] dataFork; + private byte[] resourceFork; + private String realName; + private ProdosFileInfo prodosFileInfo; + + public AppleSingle(String filename) throws IOException { + byte[] fileData = Files.readAllBytes(Paths.get(filename)); + load(fileData); + } + public AppleSingle(InputStream stream) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + StreamUtil.copy(stream, os); + os.flush(); + load(os.toByteArray()); + } + + private void load(byte[] fileData) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(fileData) + .order(ByteOrder.BIG_ENDIAN) + .asReadOnlyBuffer(); + required(buffer, MAGIC_NUMBER, "Not an AppleSingle file - magic number does not match."); + required(buffer, VERSION_NUMBER, "Only AppleSingle version 2 supported."); + buffer.position(buffer.position() + 16); // Skip filler + int entries = buffer.getShort(); + for (int i = 0; i < entries; i++) { + int entryId = buffer.getInt(); + int offset = buffer.getInt(); + int length = buffer.getInt(); + buffer.mark(); + buffer.position(offset); + byte[] entryData = new byte[length]; + buffer.get(entryData); + if (entryId == 1) { + dataFork = entryData; + } else if (entryId == 2) { + resourceFork = entryData; + } else if (entryId == 11) { + ByteBuffer infoData = ByteBuffer.wrap(entryData) + .order(ByteOrder.BIG_ENDIAN) + .asReadOnlyBuffer(); + int access = infoData.getShort(); + int fileType = infoData.getShort(); + int auxType = infoData.getInt(); + prodosFileInfo = new ProdosFileInfo(access, fileType, auxType); + } else { + throw new IOException(String.format("Unknown entry type of %04x", entryId)); + } + buffer.reset(); + } + } + private void required(ByteBuffer buffer, int expected, String message) throws IOException { + int actual = buffer.getInt(); + if (actual != expected) { + throw new IOException(String.format("%s Expected 0x%08x but read 0x%08x.", message, expected, actual)); + } + } + + public byte[] getDataFork() { + return dataFork; + } + public byte[] getResourceFork() { + return resourceFork; + } + public String getRealName() { + return realName; + } + public ProdosFileInfo getProdosFileInfo() { + return prodosFileInfo; + } + + public class ProdosFileInfo { + private int access; + private int fileType; + private int auxType; + + public ProdosFileInfo(int access, int fileType, int auxType) { + this.access = access; + this.fileType = fileType; + this.auxType = auxType; + } + + public int getAccess() { + return access; + } + public int getFileType() { + return fileType; + } + public int getAuxType() { + return auxType; + } + } +} diff --git a/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties b/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties index 8e3f876..44187b6 100644 --- a/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties +++ b/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties @@ -102,7 +102,7 @@ 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-cc65 put stdin with cc65 header\n in filename on image, using file type and address from header.\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. +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-cc65 put stdin with cc65 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. 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/AppleSingleTest.java b/src/test/java/com/webcodepro/applecommander/util/AppleSingleTest.java new file mode 100644 index 0000000..970655b --- /dev/null +++ b/src/test/java/com/webcodepro/applecommander/util/AppleSingleTest.java @@ -0,0 +1,56 @@ +package com.webcodepro.applecommander.util; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import com.webcodepro.applecommander.storage.Disk; +import com.webcodepro.applecommander.storage.DiskException; +import com.webcodepro.applecommander.storage.FileEntry; +import com.webcodepro.applecommander.storage.FormattedDisk; +import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry; +import com.webcodepro.applecommander.ui.ac; +import com.webcodepro.applecommander.util.AppleSingle.ProdosFileInfo; + +import junit.framework.TestCase; + +public class AppleSingleTest extends TestCase { + private static final String AS_HELLO_BIN = "/hello.applesingle.bin"; + + public void testSampleFromCc65() throws IOException { + AppleSingle as = new AppleSingle(getClass().getResourceAsStream(AS_HELLO_BIN)); + + assertNull(as.getRealName()); + assertNull(as.getResourceFork()); + assertNotNull(as.getDataFork()); + assertNotNull(as.getProdosFileInfo()); + + ProdosFileInfo info = as.getProdosFileInfo(); + assertEquals(0xc3, info.getAccess()); + assertEquals(0x06, info.getFileType()); + assertEquals(0x0803, info.getAuxType()); + } + + public void testViaAcTool() throws IOException, DiskException { + // Create a file that the JVM *should* delete for us. + File tmpDiskImage = File.createTempFile("deleteme-", ".po"); + tmpDiskImage.deleteOnExit(); + String tmpImageName = tmpDiskImage.getAbsolutePath(); + + // Create disk + ac.createProDisk(tmpImageName, "DELETEME", Disk.APPLE_140KB_DISK); + + // Actually test the implementation! + ac.putAppleSingle(tmpImageName, "HELLO", getClass().getResourceAsStream(AS_HELLO_BIN)); + + Disk disk = new Disk(tmpImageName); + FormattedDisk formattedDisk = disk.getFormattedDisks()[0]; + List files = formattedDisk.getFiles(); + assertNotNull(files); + assertEquals(1, files.size()); + ProdosFileEntry file = (ProdosFileEntry)files.get(0); + assertEquals("HELLO", file.getFilename()); + assertEquals("BIN", file.getFiletype()); + assertEquals(0x0803, file.getAuxiliaryType()); + } +} diff --git a/src/test/resources/hello.applesingle.bin b/src/test/resources/hello.applesingle.bin new file mode 100644 index 0000000..9bdc3cf Binary files /dev/null and b/src/test/resources/hello.applesingle.bin differ