Adding AppleSingle support #20; refactored to support minor unit

testing.
This commit is contained in:
Rob Greene 2018-03-02 12:30:58 -06:00
parent 0f83ba94b6
commit 47dc4038f3
6 changed files with 254 additions and 50 deletions

View File

@ -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<fileTypes.length; i++) {
if (filetype.equalsIgnoreCase(fileTypes[i].getString())) {
return fileTypes[i];

View File

@ -21,6 +21,7 @@
*/
package com.webcodepro.applecommander.ui;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@ -29,7 +30,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.List;
import com.webcodepro.applecommander.storage.DirectoryEntry;
@ -48,7 +48,10 @@ import com.webcodepro.applecommander.storage.physical.ByteArrayImageLayout;
import com.webcodepro.applecommander.storage.physical.DosOrder;
import com.webcodepro.applecommander.storage.physical.ImageOrder;
import com.webcodepro.applecommander.storage.physical.ProdosOrder;
import com.webcodepro.applecommander.util.AppleSingle;
import com.webcodepro.applecommander.util.AppleSingle.ProdosFileInfo;
import com.webcodepro.applecommander.util.AppleUtil;
import com.webcodepro.applecommander.util.StreamUtil;
import com.webcodepro.applecommander.util.TextBundle;
/**
@ -74,6 +77,9 @@ import com.webcodepro.applecommander.util.TextBundle;
* -n &lt;imagename&gt; &lt;volname&gt; change volume name (ProDOS or Pascal).
* -cc65 &lt;imagename&gt; &lt;filename&gt; &lt;type&gt; put stdin with cc65 header
* in filename on image, using file type and address from header.
* -as &lt;imagename&gt; [&lt;filename&gt;] put stdin with AppleSingle format
* in filename on image, using file type, address, and (optionally) name
* from the AppleSingle file.
* -geos &lt;imagename&gt; interpret stdin as a ProDOS GEOS transfer file and place on image.
* -dos140 &lt;imagename&gt; create a 140K DOS 3.3 image.
* -pro140 &lt;imagename&gt; &lt;volname&gt; create a 140K ProDOS image.
@ -125,6 +131,8 @@ public class ac {
setDiskName(args[1], args[2]);
} else if ("-cc65".equalsIgnoreCase(args[0])) { //$NON-NLS-1$
putCC65(args[1], new Name(args[2]), args[3]);
} else if ("-as".equalsIgnoreCase(args[0])) {
putAppleSingle(args[1], args.length >= 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 &lt;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<FileEntry> 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<FileEntry> 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<FileEntry> 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<FileEntry> files, String indent, int display) throws DiskException {
for (FileEntry entry : files) {
if (!entry.isDeleted()) {
List data = entry.getFileColumnData(display);
List<String> 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<FileEntry> 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<FileEntry> files = formattedDisk.getFiles();
DirectoryEntry dir = null, parentDir = null;
for (int i = 0; i < path.length - 1; i++) {
String dirName = path[i];

View File

@ -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;
}
}
}

View File

@ -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 <imagename> [<imagename>] display information about image(s).\n-ls <imagename> [<imagename>] list brief directory of image(s).\n-l <imagename> [<imagename>] list directory of image(s).\n-ll <imagename> [<imagename>] list detailed directory of image(s).\n-e <imagename> <filename> [<output>] export file from image to stdout\n or to an output file.\n-x <imagename> [<directory>] extract all files from image to directory.\n-g <imagename> <filename> [<output>] get raw file from image to stdout\n or to an output file.\n-p <imagename> <filename> <type> [[$|0x]<addr>] put stdin\n in filename on image, using file type and address [0x2000].\n-d <imagename> <filename> delete file from image.\n-k <imagename> <filename> lock file on image.\n-u <imagename> <filename> unlock file on image.\n-n <imagename> <volname> change volume name (ProDOS or Pascal).\n-cc65 <imagename> <filename> <type> put stdin with cc65 header\n in filename on image, using file type and address from header.\n-geos <imagename> interpret stdin as a GEOS conversion file and\n place it on image (ProDOS only).\n-dos140 <imagename> create a 140K DOS 3.3 image.\n-pro140 <imagename> <volname> create a 140K ProDOS image.\n-pro800 <imagename> <volname> create an 800K ProDOS image.\n-pas140 <imagename> <volname> create a 140K Pascal image.\n-pas800 <imagename> <volname> create an 800K Pascal image.\n-convert <filename> <imagename> [<sizeblocks>] 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 <imagename> [<imagename>] display information about image(s).\n-ls <imagename> [<imagename>] list brief directory of image(s).\n-l <imagename> [<imagename>] list directory of image(s).\n-ll <imagename> [<imagename>] list detailed directory of image(s).\n-e <imagename> <filename> [<output>] export file from image to stdout\n or to an output file.\n-x <imagename> [<directory>] extract all files from image to directory.\n-g <imagename> <filename> [<output>] get raw file from image to stdout\n or to an output file.\n-p <imagename> <filename> <type> [[$|0x]<addr>] put stdin\n in filename on image, using file type and address [0x2000].\n-d <imagename> <filename> delete file from image.\n-k <imagename> <filename> lock file on image.\n-u <imagename> <filename> unlock file on image.\n-n <imagename> <volname> change volume name (ProDOS or Pascal).\n-cc65 <imagename> <filename> <type> put stdin with cc65 header\n in filename on image, using file type and address from header.\n-as <imagename> [<filename>] put stdin with AppleSingle format\n in filename on image, using file type, address, and (optionally) name\n from the AppleSingle file.\n-geos <imagename> interpret stdin as a GEOS conversion file and\n place it on image (ProDOS only).\n-dos140 <imagename> create a 140K DOS 3.3 image.\n-pro140 <imagename> <volname> create a 140K ProDOS image.\n-pro800 <imagename> <volname> create an 800K ProDOS image.\n-pas140 <imagename> <volname> create a 140K Pascal image.\n-pas800 <imagename> <volname> create an 800K Pascal image.\n-convert <filename> <imagename> [<sizeblocks>] 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.

View File

@ -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<FileEntry> 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());
}
}

Binary file not shown.