package com.bytezone.diskbrowser.nib; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import com.bytezone.diskbrowser.utilities.Utility; // -----------------------------------------------------------------------------------// public class WozFile //-----------------------------------------------------------------------------------// { private static final byte[] address16prologue = { (byte) 0xD5, (byte) 0xAA, (byte) 0x96 }; private static final byte[] address13prologue = { (byte) 0xD5, (byte) 0xAA, (byte) 0xB5 }; private static final byte[] dataPrologue = { (byte) 0xD5, (byte) 0xAA, (byte) 0xAD }; private static final byte[] epilogue = { (byte) 0xDE, (byte) 0xAA, (byte) 0xEB }; // apparently it can be DE AA Ex private static final int BLOCK_SIZE = 512; private static final int SECTOR_SIZE = 256; private static final int TRK_SIZE = 0x1A00; private static final int DATA_SIZE = TRK_SIZE - 10; private static int[][] interleave = // { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, // 13 sector { 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 } }; // 16 sector public final File file; private Info info; private Meta meta; private int diskSectors; private byte[] addressPrologue; private byte[] diskBuffer; private List tracks; private final boolean debug1 = false; private final boolean showTracks = false; private final ByteTranslator6and2 byteTranslator6and2 = new ByteTranslator6and2 (); // ---------------------------------------------------------------------------------// public WozFile (File file) throws DiskNibbleException // ---------------------------------------------------------------------------------// { this.file = file; byte[] buffer = readFile (file); String header = new String (buffer, 0, 4); if (!"WOZ1".equals (header) && !"WOZ2".equals (header)) throw new DiskNibbleException ("Header error"); int checksum1 = val32 (buffer, 8); int checksum2 = Utility.crc32 (buffer, 12, buffer.length - 12); if (checksum1 != checksum2) { System.out.printf ("Stored checksum : %08X%n", checksum1); System.out.printf ("Calculated checksum : %08X%n", checksum2); throw new DiskNibbleException ("Checksum error"); } int ptr = 12; while (ptr < buffer.length) { validateChunk (buffer, ptr); String chunkId = new String (buffer, ptr, 4); int size = val32 (buffer, ptr + 4); if (debug1) System.out.printf ("%n%s %,9d%n", chunkId, size); switch (chunkId) { case "INFO": // 60 bytes info = new Info (buffer, ptr); if (info.wozVersion >= 2) setPrologue (info.bootSectorFormat == 2 ? 13 : 16); break; case "TMAP": // 160 bytes tmap (buffer, ptr); break; case "TRKS": // starts at 248, data at 256 tracks = trks (buffer, ptr); break; case "META": meta = new Meta (buffer, ptr, size); break; case "WRIT": break; default: break; } ptr += size + 8; } if (info.diskType == 1) // 5.25" { diskBuffer = new byte[tracks.size () * diskSectors * SECTOR_SIZE]; for (Track track : tracks) track.pack (diskBuffer); } else if (info.diskType == 2) // 3.5" { List sectors = new ArrayList<> (); for (Track track : tracks) sectors.addAll (track.sectors); Collections.sort (sectors); diskBuffer = new byte[800 * info.sides * BLOCK_SIZE]; ptr = 0; for (Sector sector : sectors) { sector.pack35 (diskBuffer, ptr); ptr += BLOCK_SIZE; } } } // ---------------------------------------------------------------------------------// private boolean validateChunk (byte[] buffer, int ptr) throws DiskNibbleException // ---------------------------------------------------------------------------------// { int size = val32 (buffer, ptr + 4); if (size <= 0 || size + ptr + 8 > buffer.length) { if (info != null) System.out.println (info); throw new DiskNibbleException (String.format ("Invalid chunk size: %08X%n", size)); } for (int i = 0; i < 4; i++) { int val = buffer[ptr + i] & 0xFF; if (val < 'A' || val > 'Z') // not uppercase ascii { if (info != null) System.out.println (info); throw new DiskNibbleException (String.format ("Invalid chunk name character: %02X%n", val)); } } return true; } // ---------------------------------------------------------------------------------// public byte[] getDiskBuffer () // ---------------------------------------------------------------------------------// { return diskBuffer; } // ---------------------------------------------------------------------------------// public int getDiskType () // ---------------------------------------------------------------------------------// { return info.diskType; } // ---------------------------------------------------------------------------------// public int getSides () // ---------------------------------------------------------------------------------// { return info.sides; } // ---------------------------------------------------------------------------------// public int getTracks () // ---------------------------------------------------------------------------------// { return tracks.size (); } // ---------------------------------------------------------------------------------// public int getSectorsPerTrack () // ---------------------------------------------------------------------------------// { return diskSectors; } // ---------------------------------------------------------------------------------// private void setPrologue (int diskSectors) // ---------------------------------------------------------------------------------// { this.diskSectors = diskSectors; addressPrologue = diskSectors == 13 ? address13prologue : address16prologue; } // ---------------------------------------------------------------------------------// private void tmap (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { ptr += 8; } // ---------------------------------------------------------------------------------// private List trks (byte[] rawBuffer, int ptr) // ---------------------------------------------------------------------------------// { List tracks = new ArrayList<> (); ptr += 8; int reclen = info.wozVersion == 1 ? TRK_SIZE : 8; int max = info.wozVersion == 1 ? 35 : 160; for (int i = 0; i < max; i++) { try { Track trk = new Track (i, rawBuffer, ptr); if (trk.bitCount == 0) break; tracks.add (trk); if (showTracks) System.out.printf ("%n$%02X %s%n", i, trk); } catch (DiskNibbleException e) { e.printStackTrace (); } ptr += reclen; } return tracks; } // ---------------------------------------------------------------------------------// private int val8 (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { return (buffer[ptr] & 0xFF); } // ---------------------------------------------------------------------------------// private int val16 (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { return (buffer[ptr] & 0xFF) // | ((buffer[ptr + 1] & 0xFF) << 8); } // ---------------------------------------------------------------------------------// private int val32 (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { return (buffer[ptr] & 0xFF) // | ((buffer[ptr + 1] & 0xFF) << 8) // | ((buffer[ptr + 2] & 0xFF) << 16) // | ((buffer[ptr + 3] & 0xFF) << 24); } // ---------------------------------------------------------------------------------// private int decode4and4 (byte[] buffer, int offset) // ---------------------------------------------------------------------------------// { int odds = ((buffer[offset] & 0xFF) << 1) | 0x01; int evens = buffer[offset + 1] & 0xFF; return odds & evens; } // ---------------------------------------------------------------------------------// private byte[] readFile (File file) // ---------------------------------------------------------------------------------// { try (BufferedInputStream in = new BufferedInputStream (new FileInputStream (file))) { return in.readAllBytes (); } catch (IOException e) { e.printStackTrace (); return null; } } // ---------------------------------------------------------------------------------// @Override public String toString () // ---------------------------------------------------------------------------------// { if (meta != null) return info.toString () + "\n\n" + meta.toString (); return info.toString (); } // ---------------------------------------------------------------------------------// public static void main (String[] args) // ---------------------------------------------------------------------------------// { String home = "/Users/denismolony/"; String wozBase1 = home + "Dropbox/Examples/woz test images/WOZ 1.0/"; String wozBase2 = home + "Dropbox/Examples/woz test images/WOZ 2.0/"; String wozBase3 = home + "Dropbox/Examples/woz test images/WOZ 2.0/3.5/"; File[] files = { new File (home + "code/python/wozardry-2.0/bill.woz"), new File (wozBase2 + "DOS 3.3 System Master.woz"), new File (wozBase1 + "DOS 3.3 System Master.woz"), new File (wozBase3 + "Apple IIgs System Disk 1.1.woz") }; try { new WozFile (files[3]); } catch (Exception e) { e.printStackTrace (); } } // ---------------------------------------------------------------------------------// class Info // ---------------------------------------------------------------------------------// { int wozVersion; int diskType; int writeProtected; int synchronised; int cleaned; String creator; int sides; int bootSectorFormat; int optimalBitTiming; int compatibleHardware; int requiredRam; int largestTrack; // ---------------------------------------------------------------------------------// Info (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { wozVersion = val8 (buffer, ptr + 8); diskType = val8 (buffer, ptr + 9); writeProtected = val8 (buffer, ptr + 10); synchronised = val8 (buffer, ptr + 11); cleaned = val8 (buffer, ptr + 12); creator = new String (buffer, ptr + 13, 32); if (wozVersion >= 2) { sides = val8 (buffer, ptr + 45); bootSectorFormat = val8 (buffer, ptr + 46); optimalBitTiming = val8 (buffer, ptr + 47); compatibleHardware = val16 (buffer, ptr + 48); requiredRam = val16 (buffer, ptr + 50); largestTrack = val16 (buffer, ptr + 52); } if (debug1) System.out.println (this); } // ---------------------------------------------------------------------------------// @Override public String toString () // ---------------------------------------------------------------------------------// { StringBuilder text = new StringBuilder ("WOZ info:\n\n"); String diskTypeText = diskType == 1 ? "5.25" : "3.5"; text.append (String.format ("Version ............. %d%n", wozVersion)); text.append (String.format ("Disk type ........... %d (%s\")%n", diskType, diskTypeText)); text.append (String.format ("Write protected ..... %d%n", writeProtected)); text.append (String.format ("Synchronized ........ %d%n", synchronised)); text.append (String.format ("Cleaned ............. %d%n", cleaned)); text.append (String.format ("Creator ............. %s", creator)); if (wozVersion > 1) { String bootSectorFormatText = bootSectorFormat == 0 ? "Unknown" : bootSectorFormat == 1 ? "16 sector" : bootSectorFormat == 2 ? "13 sector" : "Hybrid"; text.append (String.format ("%nSides ............... %d%n", sides)); text.append (String.format ("Boot sector format .. %d (%s)%n", bootSectorFormat, bootSectorFormatText)); text.append (String.format ("Optimal bit timing .. %d%n", optimalBitTiming)); text.append (String.format ("Compatible hardware . %d%n", compatibleHardware)); text.append (String.format ("Required RAM ........ %d%n", requiredRam)); text.append (String.format ("Largest track ....... %d", largestTrack)); } return text.toString (); } } // -----------------------------------------------------------------------------------// class Meta // -----------------------------------------------------------------------------------// { List lines = new ArrayList<> (); // ---------------------------------------------------------------------------------// Meta (byte[] buffer, int ptr, int length) // ---------------------------------------------------------------------------------// { String dots = " ......................"; String metaData = new String (buffer, ptr + 8, length); String[] chunks = metaData.split ("\n"); for (String chunk : chunks) { String[] parts = chunk.split ("\t"); if (parts.length >= 2) lines.add (String.format ("%-21.21s %s", parts[0] + dots, parts[1])); else lines.add (String.format ("%-21.21s", parts[0] + dots)); } } // ---------------------------------------------------------------------------------// @Override public String toString () // ---------------------------------------------------------------------------------// { StringBuilder text = new StringBuilder ("WOZ meta:\n\n"); for (String line : lines) text.append (String.format ("%s%n", line)); if (text.length () > 0) text.deleteCharAt (text.length () - 1); return text.toString (); } } // -----------------------------------------------------------------------------------// class Track implements Iterable // -----------------------------------------------------------------------------------// { private int trackNo; private int startingBlock; private int blockCount; // WOZ2 - not needed private int bitCount; private int bytesUsed; // WOZ1 - not needed private byte[] rawBuffer; private byte[] newBuffer; private int bitIndex; private int byteIndex; private int trackIndex; private int revolutions; List sectors = new ArrayList<> (); // ---------------------------------------------------------------------------------// public Track (int trackNo, byte[] rawBuffer, int ptr) throws DiskNibbleException // ---------------------------------------------------------------------------------// { this.rawBuffer = rawBuffer; this.trackNo = trackNo; if (info.wozVersion == 1) { bytesUsed = val16 (rawBuffer, ptr + DATA_SIZE); bitCount = val16 (rawBuffer, ptr + DATA_SIZE + 2); if (debug1) System.out.println ((String.format ("Bytes: %2d, Bits: %,8d%n%n", bytesUsed, bitCount))); } else { startingBlock = val16 (rawBuffer, ptr); blockCount = val16 (rawBuffer, ptr + 2); bitCount = val32 (rawBuffer, ptr + 4); if (debug1) System.out.println ((String.format ("%nStart: %4d, Blocks: %2d, Bits: %,8d%n", startingBlock, blockCount, bitCount))); } if (bitCount == 0) return; resetIndex (); if (addressPrologue == null) // WOZ1 if (findNext (address16prologue, ptr) > 0) setPrologue (16); else if (findNext (address13prologue, ptr) > 0) setPrologue (13); else throw new DiskNibbleException ("No address prologue found"); int offset = -1; while (sectors.size () < diskSectors) { offset = findNext (addressPrologue, offset + 1); if (offset < 0) break; Sector sector = new Sector (this, offset); if (isDuplicate (sector)) break; sectors.add (sector); } } // ---------------------------------------------------------------------------------// private boolean isDuplicate (Sector newSector) // ---------------------------------------------------------------------------------// { for (Sector sector : sectors) if (sector.sectorNo == newSector.sectorNo) return true; return false; } // ---------------------------------------------------------------------------------// private void resetIndex () // ---------------------------------------------------------------------------------// { trackIndex = 0; bitIndex = 0; if (info.wozVersion == 1) byteIndex = 256 + trackNo * TRK_SIZE; else byteIndex = startingBlock * BLOCK_SIZE; } // ---------------------------------------------------------------------------------// boolean nextBit () // ---------------------------------------------------------------------------------// { boolean bit = (rawBuffer[byteIndex] & (0x80 >>> bitIndex)) != 0; if (++trackIndex >= bitCount) { ++revolutions; resetIndex (); } else if (++bitIndex >= 8) { ++byteIndex; bitIndex = 0; } return bit; } // ---------------------------------------------------------------------------------// int nextByte () // ---------------------------------------------------------------------------------// { byte b = 0; while ((b & 0x80) == 0) { b <<= 1; if (nextBit ()) b |= 0x01; } return b; } // ---------------------------------------------------------------------------------// void readTrack () // ---------------------------------------------------------------------------------// { if (newBuffer != null) return; int max = (bitCount - 1) / 8 + 1; max += 600; newBuffer = new byte[max]; for (int i = 0; i < max; i++) newBuffer[i] = (byte) nextByte (); } // ---------------------------------------------------------------------------------// int findNext (byte[] key, int start) // ---------------------------------------------------------------------------------// { readTrack (); int max = newBuffer.length - key.length; outer: for (int ptr = start; ptr < max; ptr++) { for (int keyPtr = 0; keyPtr < key.length; keyPtr++) if (newBuffer[ptr + keyPtr] != key[keyPtr]) continue outer; return ptr; } return -1; } // ---------------------------------------------------------------------------------// void pack (byte[] diskBuffer) throws DiskNibbleException // ---------------------------------------------------------------------------------// { int ndx = diskSectors == 13 ? 0 : 1; DiskReader diskReader = DiskReader.getInstance (diskSectors); for (Sector sector : sectors) if (sector.dataOffset > 0) { byte[] decodedBuffer = diskReader.decodeSector (newBuffer, sector.dataOffset + 3); int ptr = SECTOR_SIZE * (sector.trackNo * diskSectors + interleave[ndx][sector.sectorNo]); System.arraycopy (decodedBuffer, 0, diskBuffer, ptr, decodedBuffer.length); } } // ---------------------------------------------------------------------------------// @Override public String toString () // ---------------------------------------------------------------------------------// { StringBuilder text = new StringBuilder (); if (info.wozVersion == 1) text.append (String.format ("WOZ1: Bytes: %2d, Bits: %,8d%n%n", bytesUsed, bitCount)); else text.append (String.format ("WOZ2: Start: %4d, Blocks: %2d, Bits: %,8d%n%n", startingBlock, blockCount, bitCount)); int count = 0; for (Sector sector : sectors) text.append (String.format ("%2d %s%n", count++, sector)); text.deleteCharAt (text.length () - 1); return text.toString (); } // ---------------------------------------------------------------------------------// @Override public Iterator iterator () // ---------------------------------------------------------------------------------// { return sectors.iterator (); } } // ---------------------------------------------------------------------------------// public class Sector implements Comparable // ---------------------------------------------------------------------------------// { private final Track track; private int trackNo, sectorNo, volume, checksum; private final int addressOffset; private int dataOffset; // ---------------------------------------------------------------------------------// Sector (Track track, int addressOffset) // ---------------------------------------------------------------------------------// { this.track = track; if (info.diskType == 1) { volume = decode4and4 (track.newBuffer, addressOffset + 3); trackNo = decode4and4 (track.newBuffer, addressOffset + 5); sectorNo = decode4and4 (track.newBuffer, addressOffset + 7); checksum = decode4and4 (track.newBuffer, addressOffset + 9); } else { // http://apple2.guidero.us/doku.php/articles/iicplus_smartport_secrets // SWIM Chip User's Ref pp 6 // uPD72070.pdf try { int b1 = byteTranslator6and2.decode (track.newBuffer[addressOffset + 3]); sectorNo = byteTranslator6and2.decode (track.newBuffer[addressOffset + 4]); int b3 = byteTranslator6and2.decode (track.newBuffer[addressOffset + 5]); int format = byteTranslator6and2.decode (track.newBuffer[addressOffset + 6]); checksum = byteTranslator6and2.decode (track.newBuffer[addressOffset + 7]); trackNo = (b1 & 0x3F) | ((b3 & 0x1F) << 6); volume = (b3 & 0x20) >>> 5; // side int chk = b1 ^ sectorNo ^ b3 ^ format; assert chk == checksum; } catch (DiskNibbleException e) { e.printStackTrace (); } } // int epiloguePtr = track.findNext (epilogue, addressOffset + 11); // assert epiloguePtr == addressOffset + 11; this.addressOffset = addressOffset; dataOffset = track.findNext (dataPrologue, addressOffset + 11); if (dataOffset > addressOffset + 200) dataOffset = -1; } // ---------------------------------------------------------------------------------// void pack35 (byte[] diskBuffer, int ptr) throws DiskNibbleException // ---------------------------------------------------------------------------------// { DiskReader diskReader = DiskReader.getInstance (0); // start decoding from 4 bytes past the data prologue (3 bytes for prologue // itself, and another byte for the sector number) byte[] decodedBuffer = diskReader.decodeSector (track.newBuffer, dataOffset + 4); // return 512 bytes (ignore the 12 tag bytes) System.arraycopy (decodedBuffer, DiskReaderGCR.TAG_SIZE, diskBuffer, ptr, 512); } // ---------------------------------------------------------------------------------// @Override public String toString () // ---------------------------------------------------------------------------------// { String fld = info.diskType == 1 ? "Vol" : info.diskType == 2 ? "Sde" : "???"; String dataOffsetText = dataOffset < 0 ? "" : String.format ("%04X", dataOffset); return String.format ("%s: %02X Trk: %02X Sct: %02X Chk: %02X Add: %04X Dat: %s", fld, volume, trackNo, sectorNo, checksum, addressOffset, dataOffsetText); } // ---------------------------------------------------------------------------------// @Override public int compareTo (Sector o) // ---------------------------------------------------------------------------------// { if (this.trackNo != o.trackNo) return this.trackNo - o.trackNo; if (this.volume != o.volume) // side return this.volume - o.volume; return this.sectorNo - o.sectorNo; } } }