import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.util.List; import java.util.zip.GZIPInputStream; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.Disk; import com.webcodepro.applecommander.storage.DiskFullException; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; /* * This class uses AppleCommander's command-line interface to extract an entire * set of files and directories from an image, or build a whole image from * files and directories. * * Has specific hard-coded addresses for Lawless Legends files; should fix this * at some point to use a metadata configuration file or something like that. */ public class A2copy { /* * Main command-line driver */ public static void main(String[] args) throws IOException, DiskFullException { try { if (args[0].equals("-extract")) { extractImg(args[1], args[2]); return; } if (args[0].equals("-create")) { createImg(args[1], args[2]); return; } } catch (ArrayIndexOutOfBoundsException e) { } System.err.format("Usage: A2copy [-extract imgFile targetDir] | [-create imgFile sourceDir]\n"); System.exit(1); } /** * Helper to delete a file or directory and all its descendants. * @throws IOException if something goes wrong */ static void delete(File f) throws IOException { if (f.isDirectory()) { for (File c : f.listFiles()) delete(c); } if (!f.delete()) throw new FileNotFoundException("Failed to delete file: " + f); } /** * Extract all the files and directories from an image file. * @throws IOException if something goes wrong */ @SuppressWarnings("unchecked") static void extractImg(String imgPath, String targetPath) throws IOException { // Ensure the target directory is empty first. File targetDir = new File(targetPath); if (targetDir.exists()) { System.out.format("Note: %s will be overwritten. Continue? ", targetPath); System.out.flush(); String response = new BufferedReader(new InputStreamReader(System.in)).readLine(); if (!response.toLowerCase().startsWith("y")) return; delete(targetDir); } // Open the image file and process disks inside it. Disk disk = new Disk(imgPath); for (FormattedDisk fd : disk.getFormattedDisks()) extractFiles((List)fd.getFiles(), targetDir); } /** * Helper for file/directory extraction. * @param files set of files to extract * @param targetDir where to put them * @throws IOException if something goes wrong */ @SuppressWarnings("unchecked") static void extractFiles(List files, File targetDir) throws IOException { // Ensure the target directory exists targetDir.mkdir(); // Process each entry in the list for (FileEntry e : files) { // Skip deleted things if (e.isDeleted()) continue; // Recursively process sub-directories if (e.isDirectory()) { File subDir = new File(targetDir, e.getFilename()); extractFiles(((DirectoryEntry)e).getFiles(), subDir); continue; } // Process regular files. byte[] data = e.getFileData(); // Hi to lo ASCII translation if (e.getFilename().endsWith(".S")) data = merlinSrcToAscii(data); FileOutputStream out = new FileOutputStream(new File(targetDir, e.getFilename())); out.write(data); out.close(); } } /** * Creates an image file using dirs/files from the filesystem. * @param imgPath name of the image file * @param srcDirPath directory containing files and subdirs * @throws DiskFullException if the image file fills up * @throws IOException if something else goes wrong */ static void createImg(String imgPath, String srcDirPath) throws IOException, DiskFullException { // Alert the user that we're going to blow away the image. File imgFile = new File(imgPath); if (imgFile.exists()) { System.out.format("Note: %s will be overwritten. Continue? ", imgPath); System.out.flush(); String response = new BufferedReader(new InputStreamReader(System.in)).readLine(); if (!response.toLowerCase().startsWith("y")) return; delete(imgFile); } // Re-create the image file with a blank image. File emptyFile = new File(imgFile.getParent(), "empty.2mg.gz"); if (!emptyFile.canRead()) throw new IOException(String.format("Cannot open template for empty image '%s'", emptyFile.toString())); InputStream in = new BufferedInputStream(new GZIPInputStream(new FileInputStream(emptyFile))); OutputStream out = new BufferedOutputStream(new FileOutputStream(imgFile)); byte[] buf = new byte[1024]; while (true) { int nRead = in.read(buf); if (nRead < 0) break; out.write(buf, 0, nRead); } in.close(); out.close(); // Open the empty image file. Disk disk = new Disk(imgPath); FormattedDisk fd = disk.getFormattedDisks()[0]; // And fill it up. insertFiles(fd, fd, new File(srcDirPath)); } /** * Helper for image creation. * * @param fd disk to insert files into * @param targetDir directory within the disk * @param srcDir filesystem directory to read * @throws DiskFullException if the image file fills up * @throws IOException if something else goes wrong */ private static void insertFiles(FormattedDisk fd, DirectoryEntry targetDir, File srcDir) throws DiskFullException, IOException { // Process each file in the source directory for (File srcFile : srcDir.listFiles()) { if (srcFile.isDirectory()) { DirectoryEntry subDir = targetDir.createDirectory(srcFile.getName().toUpperCase()); insertFiles(fd, subDir, srcFile); continue; } // Create a new entry on the filesystem for this file. FileEntry ent = targetDir.createFile(); String srcName = srcFile.getName().toUpperCase(); ent.setFilename(srcName); // Set the file type if (srcName.equals("PRODOS") || srcName.endsWith(".SYSTEM")) ent.setFiletype("SYS"); else if (srcName.equals("STARTUP")) ent.setFiletype("BAS"); else if (srcName.endsWith(".S")) ent.setFiletype("TXT"); else ent.setFiletype("BIN"); // Set the address if necessary if (ent.needsAddress()) { if (srcName.equals("STARTUP")) ent.setAddress(0x801); else if (srcName.equals("COPYIIPL.SYSTEM")) ent.setAddress(0x1400); else if (srcName.equals("ED.16")) ent.setAddress(0x9d60); else if (srcName.equals("ED")) ent.setAddress(0x9db6); else if (srcName.equals("SHELL")) ent.setAddress(0x300); else ent.setAddress(0x2000); } // Copy the file data FileInputStream in = new FileInputStream(srcFile); byte[] buf = new byte[(int) srcFile.length()]; int nRead = in.read(buf); if (nRead != srcFile.length()) throw new IOException(String.format("Error reading file '%s'", srcFile.toString())); // Translate between hi and lo ASCII if (srcName.endsWith(".S")) buf = asciiToMerlinSrc(buf); ent.setFileData(buf); // And save the new entry. fd.save(); } } /** * Translates Merlin source code to usable code in the regular ASCII world. * Performs weird-space to tab translation, and hi bit conversion. * * @param buf data to translate * @return translated data */ static byte[] merlinSrcToAscii(byte[] buf) { ByteArrayOutputStream ba = new ByteArrayOutputStream(buf.length); PrintWriter out = new PrintWriter(ba); boolean inComment = false; for (byte b : buf) { // Handle newlines if (b == (byte)0x8d) { out.println(); inComment = false; } else { char c = (char)(b & 0x7f); // Tabs outside comments if (c == ';' || c == '*') inComment = true; if (c == '\n') throw new RuntimeException("Newline slipped through"); if (c == ' ' && !inComment) out.write('\t'); else out.write(c); } } out.flush(); return ba.toByteArray(); } /** * Transforms regular ASCII with tabs to Merlin source code. Handles * tab to weird space translation, and hi-bit addition. * * @param buf data to translate * @return translated data */ static byte[] asciiToMerlinSrc(byte[] buf) { ByteArrayOutputStream ba = new ByteArrayOutputStream(buf.length); boolean inComment = false; for (int i=0; i