Adding some rudimentary WOZ image handling. 5.25" only. Unprotected disks only, of course.

This commit is contained in:
Rob Greene
2025-07-19 09:43:09 -05:00
parent 6c2be6ddfb
commit 818a9f0e56
3 changed files with 281 additions and 7 deletions
@@ -39,12 +39,7 @@ import com.webcodepro.applecommander.storage.os.gutenberg.GutenbergFormatDisk;
import com.webcodepro.applecommander.storage.os.pascal.PascalFormatDisk;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
import com.webcodepro.applecommander.storage.os.rdos.RdosFormatDisk;
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.NibbleOrder;
import com.webcodepro.applecommander.storage.physical.ProdosOrder;
import com.webcodepro.applecommander.storage.physical.UniversalDiskImageLayout;
import com.webcodepro.applecommander.storage.physical.*;
import com.webcodepro.applecommander.util.AppleUtil;
import com.webcodepro.applecommander.util.StreamUtil;
import com.webcodepro.applecommander.util.TextBundle;
@@ -130,7 +125,7 @@ public class Disk {
private Disk() {
filenameFilters = new FilenameFilter[] {
new FilenameFilter(textBundle.get("Disk.AllImages"), //$NON-NLS-1$
"*.do", "*.dsk", "*.po", "*.nib", "*.2mg", "*.2img", "*.hdv", "*.do.gz", "*.dsk.gz", "*.po.gz", "*.nib.gz", "*.2mg.gz", "*.2img.gz"), //$NON-NLS-1$
"*.do", "*.dsk", "*.po", "*.nib", "*.2mg", "*.2img", "*.hdv", "*.do.gz", "*.dsk.gz", "*.po.gz", "*.nib.gz", "*.2mg.gz", "*.2img.gz", "*.woz"), //$NON-NLS-1$
new FilenameFilter(textBundle.get("Disk.140kDosImages"), //$NON-NLS-1$
"*.do", "*.dsk", "*.do.gz", "*.dsk.gz"), //$NON-NLS-1$
new FilenameFilter(textBundle.get("Disk.140kNibbleImages"), //$NON-NLS-1$
@@ -143,6 +138,8 @@ public class Disk {
"*.hdv"), //$NON-NLS-1$
new FilenameFilter(textBundle.get("Disk.CompressedImages"), //$NON-NLS-1$
"*.sdk", "*.shk", "*.do.gz", "*.dsk.gz", "*.po.gz", "*.2mg.gz", "*.2img.gz"), //$NON-NLS-1$
new FilenameFilter(textBundle.get("Disk.WozImages"),
"*.woz"),
new FilenameFilter(textBundle.get("Disk.AllFiles"), //$NON-NLS-1$
"*.*") //$NON-NLS-1$
};
@@ -229,10 +226,15 @@ public class Disk {
diskImage = diskImageByteArray.toByteArray();
}
boolean is2img = false;
boolean isWoz = false;
/* Does it have the 2IMG header? */
if ((diskImage[0] == 0x32) && (diskImage[1] == 0x49) && (diskImage[2] == 0x4D) && (diskImage[3]) == 0x47) {
is2img = true;
}
/* Does it have the WOZ header? */
else if ((diskImage[0] == 0x57) && (diskImage[1] == 0x4f) && (diskImage[2] == 0x5a) && (diskImage[3]) == 0x32) {
isWoz = true;
}
/* Does it have the DiskCopy 4.2 header? */
else if (Disk.isDC42(diskImage)) {
isDC42 = true;
@@ -265,6 +267,8 @@ public class Disk {
if (isSDK()) {
imageOrder = proDosOrder; // SDKs are always in ProDOS order
} else if (isWoz) {
imageOrder = new WozOrder(diskImageManager);
} else {
/*
* First step: test physical disk orders for viable file systems.
@@ -519,6 +523,10 @@ public class Disk {
* Identify the size of this disk.
*/
public int getPhysicalSize() {
if (getImageOrder() instanceof WozOrder) {
// Total hack since WOZ is currently a special case.
return getImageOrder().getPhysicalSize();
}
if (getDiskImageManager() != null) {
return getDiskImageManager().getPhysicalSize();
}
@@ -0,0 +1,265 @@
package com.webcodepro.applecommander.storage.physical;
import com.webcodepro.applecommander.storage.Disk;
import com.webcodepro.applecommander.util.AppleUtil;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WozOrder extends NibbleOrder {
public static final int INFO_CHUNK_ID = 0x4f464e49;
public static final int TMAP_CHUNK_ID = 0x50414d54;
public static final int TRKS_CHUNK_ID = 0x534b5254;
public static final int FLUX_CHUNK_ID = 0x54495257;
public static final int META_CHUNK_ID = 0x4154454d;
private InfoChunk info;
private Map<String,String> meta = new HashMap<>();
private List<TmapChunk> tmap = new ArrayList<>();
private List<TrkInfo> trks = new ArrayList<>();
public WozOrder(ByteArrayImageLayout layout) {
super(layout);
ByteBuffer bb = ByteBuffer.wrap(layout.getDiskImage());
bb.order(ByteOrder.LITTLE_ENDIAN);
int sig = bb.getInt();
int test = bb.getInt();
if (sig != 0x325a4f57 || test != 0xa0d0aff) {
throw new RuntimeException("Not a WOZ v2 format file.");
}
bb.getInt(); // ignoring CRC
while (bb.hasRemaining()) {
int chunkId = bb.getInt();
int chunkSize = bb.getInt();
byte[] data = new byte[chunkSize];
bb.get(data);
switch (chunkId) {
case INFO_CHUNK_ID:
this.info = new InfoChunk(data);
if (this.info.getDiskType() != 1) {
throw new RuntimeException("WOZ support only supports 5.25\" disks at this time");
}
break;
case TMAP_CHUNK_ID:
readTmapChunk(data);
break;
case TRKS_CHUNK_ID:
readTrksChunk(data);
case META_CHUNK_ID:
readMetaChunk(data);
break;
}
}
}
@Override
public int getPhysicalSize() {
return Disk.APPLE_140KB_NIBBLE_DISK;
}
private void readMetaChunk(byte[] data) {
if (data.length > 0) {
String meta = new String(data);
for (String line : meta.split("\n")) {
String[] parts = line.split("\t");
if (parts.length == 2) {
this.meta.put(parts[0], parts[1]);
}
}
}
}
private void readTmapChunk(byte[] data) {
ByteBuffer bb = ByteBuffer.wrap(data);
while (bb.hasRemaining()) {
byte[] chunk = new byte[4];
bb.get(chunk);
tmap.add(new TmapChunk(chunk));
}
}
private void readTrksChunk(byte[] data) {
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (int i=0; i<160; i++) {
trks.add(new TrkInfo(bb));
}
}
@Override
protected byte[] readTrackData(int track) {
TmapChunk map = tmap.get(track);
int trkInfo = map.getOffset00();
if (trkInfo == 255) trkInfo = map.getOffset25();
if (trkInfo == 255) trkInfo = map.getOffset50();
if (trkInfo == 255) trkInfo = map.getOffset75();
TrkInfo trk = trks.get(trkInfo);
byte[] rawData = readBytes(trk.getStartingBlock()*512, trk.getBlockCount()*512);
return transformBitstream(rawData, trk.getBitCount());
}
protected byte[] transformBitstream(byte[] rawData, int bitCount) {
// NOTE: Uncertain if we need to track 0's (only 2 in a row) or not.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bitNo = 7;
int byteNo = 0;
int byteVal = 0;
while (bitCount > 0) {
bitCount--;
byteVal <<= 1;
if (AppleUtil.isBitSet(rawData[byteNo], bitNo)) {
byteVal |= 1;
}
bitNo--;
if (bitNo < 0) {
bitNo = 7;
byteNo++;
}
if (byteVal >= 128) {
baos.write(byteVal);
byteVal = 0;
}
}
return baos.toByteArray();
}
@Override
protected void writeTrackData(int track, byte[] trackData) {
throw new RuntimeException("WOZ disks do not support writing");
}
public static class InfoChunk {
private int version;
private int diskType;
private int writeProtected;
private int synchrinized;
private int cleaned;
private String creator;
private int diskSides;
private int bootSectorFormat;
private int optimalBitTiming;
private int compatibleHardware;
private int requiredRAM;
private int largestTrack;
private int fluxBlock;
private int largestFluxTrack;
public InfoChunk(byte[] data) {
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.LITTLE_ENDIAN);
this.version = bb.get();
this.diskType = bb.get();
this.writeProtected = bb.get();
this.synchrinized = bb.get();
this.cleaned = bb.get();
byte[] creator = new byte[32];
bb.get(creator);
this.creator = new String(creator);
this.diskSides = bb.get();
this.bootSectorFormat = bb.get();
this.optimalBitTiming = bb.get();
this.compatibleHardware = bb.getShort();
this.requiredRAM = bb.getShort();
this.largestTrack = bb.getShort();
this.fluxBlock = bb.getShort();
this.largestFluxTrack = bb.getShort();
}
public int getVersion() {
return version;
}
public int getDiskType() {
return diskType;
}
public int getWriteProtected() {
return writeProtected;
}
public int getSynchrinized() {
return synchrinized;
}
public int getCleaned() {
return cleaned;
}
public String getCreator() {
return creator;
}
public int getDiskSides() {
return diskSides;
}
public int getBootSectorFormat() {
return bootSectorFormat;
}
public int getOptimalBitTiming() {
return optimalBitTiming;
}
public int getCompatibleHardware() {
return compatibleHardware;
}
public int getRequiredRAM() {
return requiredRAM;
}
public int getLargestTrack() {
return largestTrack;
}
public int getFluxBlock() {
return fluxBlock;
}
public int getLargestFluxTrack() {
return largestFluxTrack;
}
}
public static class TmapChunk {
private byte[] tmap;
public TmapChunk(byte[] data) {
if (data.length != 4) {
throw new RuntimeException("Unexpected TMAP chunk size of " + data.length);
}
tmap = data;
}
public int getOffset00() {
return Byte.toUnsignedInt(tmap[0]);
}
public int getOffset25() {
return Byte.toUnsignedInt(tmap[1]);
}
public int getOffset50() {
return Byte.toUnsignedInt(tmap[2]);
}
public int getOffset75() {
return Byte.toUnsignedInt(tmap[3]);
}
}
public static class TrkInfo {
private int startingBlock;
private int blockCount;
private int bitCount;
public TrkInfo(ByteBuffer bb) {
this.startingBlock = bb.getShort();
this.blockCount = bb.getShort();
this.bitCount = bb.getInt();
}
public int getStartingBlock() {
return startingBlock;
}
public int getBlockCount() {
return blockCount;
}
public int getBitCount() {
return bitCount;
}
}
}
@@ -36,6 +36,7 @@ Disk.140kDosImages=140K DOS Ordered Images (*.do, *.dsk)
Disk.140kNibbleImages=140K Nibbilized Images (*.nib)
Disk.140kProdosImages=140K ProDOS Ordered Images (*.po)
Disk.800kProdosImages=800K ProDOS Ordered Images (*.2mg, *.2img)
Disk.WozImages=WOZ 2.x Images (*.woz)
Disk.ApplePcImages=ApplePC Hard Disk Images (*.hdv)
Disk.CompressedImages=All Compressed Images
Disk.AllFiles=All Files