dmolony-DiskBrowser/src/com/bytezone/diskbrowser/nib/WozFile.java

739 lines
25 KiB
Java

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<Track> 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<Sector> 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<Track> trks (byte[] rawBuffer, int ptr)
// ---------------------------------------------------------------------------------//
{
List<Track> 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<String> 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<Sector>
// -----------------------------------------------------------------------------------//
{
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<Sector> 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<Sector> iterator ()
// ---------------------------------------------------------------------------------//
{
return sectors.iterator ();
}
}
// ---------------------------------------------------------------------------------//
public class Sector implements Comparable<Sector>
// ---------------------------------------------------------------------------------//
{
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;
}
}
}