Changing the TrackSectorToBlockAdapter to (a) rely on the given TrackSectorDevice to handle sector skew (via the SkewedTrackSectorDevice) and (b) handle varying block sizes (RDOS, ProDOS/Pascal, CP/M).

This commit is contained in:
Rob Greene
2025-08-29 19:56:16 -05:00
parent ad13e644c2
commit e2dc08db54
11 changed files with 176 additions and 37 deletions
@@ -22,7 +22,10 @@ package com.webcodepro.applecommander.storage.os.pascal;
import com.webcodepro.applecommander.storage.DiskConstants;
import com.webcodepro.applecommander.storage.DiskFactory;
import org.applecommander.device.BlockDevice;
import org.applecommander.device.SkewedTrackSectorDevice;
import org.applecommander.device.TrackSectorDevice;
import org.applecommander.device.TrackSectorToBlockAdapter;
import org.applecommander.hint.Hint;
import org.applecommander.util.DataBuffer;
/**
@@ -37,9 +40,18 @@ public class PascalDiskFactory implements DiskFactory {
}
}
if (ctx.sectorDevice != null && ctx.sectorDevice.getGeometry().sectorsPerDisk() <= 1600) {
BlockDevice device = new TrackSectorToBlockAdapter(ctx.sectorDevice);
if (check(device)) {
ctx.disks.add(new PascalFormatDisk(ctx.source.getName(), device));
TrackSectorDevice skewed = null;
if (ctx.sectorDevice.is(Hint.NIBBLE_SECTOR_ORDER)) {
skewed = SkewedTrackSectorDevice.physicalToPascalSkew(ctx.sectorDevice);
}
else if (ctx.sectorDevice.is(Hint.DOS_SECTOR_ORDER)) {
skewed = SkewedTrackSectorDevice.dosToPascalSkew(ctx.sectorDevice);
}
if (skewed != null) {
BlockDevice device = new TrackSectorToBlockAdapter(skewed, TrackSectorToBlockAdapter.BlockStyle.PASCAL);
if (check(device)) {
ctx.disks.add(new PascalFormatDisk(ctx.source.getName(), device));
}
}
}
}
@@ -21,7 +21,10 @@ package com.webcodepro.applecommander.storage.os.prodos;
import com.webcodepro.applecommander.storage.DiskFactory;
import org.applecommander.device.BlockDevice;
import org.applecommander.device.SkewedTrackSectorDevice;
import org.applecommander.device.TrackSectorDevice;
import org.applecommander.device.TrackSectorToBlockAdapter;
import org.applecommander.hint.Hint;
import org.applecommander.util.DataBuffer;
import static com.webcodepro.applecommander.storage.DiskConstants.*;
@@ -37,8 +40,17 @@ public class ProdosDiskFactory implements DiskFactory {
tests.add(new ProdosFormatDisk(ctx.source.getName(), ctx.blockDevice));
}
if (ctx.sectorDevice != null && ctx.sectorDevice.getGeometry().sectorsPerDisk() <= 1600) {
BlockDevice device = new TrackSectorToBlockAdapter(ctx.sectorDevice);
tests.add(new ProdosFormatDisk(ctx.source.getName(), device));
TrackSectorDevice skewed = null;
if (ctx.sectorDevice.is(Hint.NIBBLE_SECTOR_ORDER)) {
skewed = SkewedTrackSectorDevice.physicalToPascalSkew(ctx.sectorDevice);
}
else if (ctx.sectorDevice.is(Hint.DOS_SECTOR_ORDER)) {
skewed = SkewedTrackSectorDevice.dosToPascalSkew(ctx.sectorDevice);
}
if (skewed != null) {
BlockDevice device = new TrackSectorToBlockAdapter(skewed, TrackSectorToBlockAdapter.BlockStyle.PRODOS);
tests.add(new ProdosFormatDisk(ctx.source.getName(), device));
}
}
// ... and then test for ProDOS details:
for (ProdosFormatDisk fdisk : tests) {
@@ -45,5 +45,9 @@ public interface BlockDevice extends Device {
}
}
record Geometry(int blockSize, int blocksOnDevice) {}
record Geometry(int blockSize, int blocksOnDevice) {
public int deviceSize() {
return blocksOnDevice * blockSize;
}
}
}
@@ -26,12 +26,62 @@ import org.applecommander.util.DataBuffer;
import java.util.Optional;
/**
* This is an overlay on a TrackSectorDevice to give the proper sector skew to the device.
* <p/>
* <pre>
* From Beneath Apple DOS (2020).
* -------- SECTOR ORDERING --------
* Physical* DOS 3.3 Pascal* CP/M
* 0 0 0 0
* 1 D 2 3
* 2 B 4 6
* 3 9 6 9
* 4 7 8 C
* 5 5 A F
* 6 3 C 2
* 7 1 E 5
* 8 E 1 8
* 9 C 3 B
* A A 5 E
* B 8 7 1
* C 6 9 4
* D 4 B 7
* E 2 D A
* F F F D
* </pre>
* <ul>Notes:
* <li>Physical = RDOS and DOS 3.2 sector ordering</li>
* <li>Pascal = ProDOS sector ordering</li>
* </ul>
*/
public class SkewedTrackSectorDevice implements TrackSectorDevice {
public static TrackSectorDevice dosToPhysicalSkew(TrackSectorDevice device) {
public static TrackSectorDevice physicalToDosSkew(TrackSectorDevice device) {
return new SkewedTrackSectorDevice(device,
0x0, 0xd, 0xb, 0x9, 0x7, 0x5, 0x3, 0x1,
0xe, 0xc, 0xa, 0x8, 0x6, 0x4, 0x2, 0xf);
}
public static TrackSectorDevice physicalToPascalSkew(TrackSectorDevice device) {
return new SkewedTrackSectorDevice(device,
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf);
}
public static TrackSectorDevice dosToPhysicalSkew(TrackSectorDevice device) {
return new SkewedTrackSectorDevice(device,
0x0, 0x7, 0xe, 0x6, 0xd, 0x5, 0xc, 0x4,
0xb, 0x3, 0xa, 0x2, 0x9, 0x1, 0x8, 0xf);
}
public static TrackSectorDevice dosToPascalSkew(TrackSectorDevice device) {
return new SkewedTrackSectorDevice(device,
0x0, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8,
0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0xf);
}
// CP/M skews are from 'cpmtools'
public static TrackSectorDevice dosToCpmSkew(TrackSectorDevice device) {
return new SkewedTrackSectorDevice(device,
0x0, 0x6, 0xc, 0x3, 0x9, 0xf, 0xe, 0x5,
0xb, 0x2, 0x8, 0x7, 0xd, 0x4, 0xa, 0x1);
}
private final TrackSectorDevice device;
private final int[] sectorSkew;
@@ -45,5 +45,8 @@ public interface TrackSectorDevice extends Device {
public int sectorsPerDisk() {
return tracksOnDisk*sectorsPerTrack;
}
public int deviceSize() {
return sectorsPerDisk() * SECTOR_SIZE;
}
}
}
@@ -25,15 +25,22 @@ import org.applecommander.util.Container;
import org.applecommander.util.DataBuffer;
import java.util.Optional;
import java.util.function.BiConsumer;
/**
* Provides a _very simple_ mapping from Track/Sectors to blocks. All sector skewing is expected
* to be handled in the TrackSectorDevice. As such, this device expects to map sectors in the given
* ("natural") order. (That is, ProDOS blocks are sectors 0,1 and 2,3 and 3,4 etc. RDOS is in physical
* order. CP/M is in CP/M's expected order.)
*/
public class TrackSectorToBlockAdapter implements BlockDevice {
private final TrackSectorDevice device;
private final BlockStyle style;
private final Geometry geometry;
public TrackSectorToBlockAdapter(TrackSectorDevice device) {
public TrackSectorToBlockAdapter(TrackSectorDevice device, BlockStyle style) {
this.device = device;
this.geometry = new Geometry(STANDARD_BLOCK_SIZE, device.getGeometry().sectorsPerDisk() / 2);
this.style = style;
this.geometry = new Geometry(style.blockSize, device.getGeometry().deviceSize() / style.blockSize);
}
@Override
@@ -43,7 +50,7 @@ public class TrackSectorToBlockAdapter implements BlockDevice {
@Override
public boolean is(Hint hint) {
return device.is(hint);
return hint == Hint.PRODOS_BLOCK_ORDER;
}
@Override
@@ -58,33 +65,52 @@ public class TrackSectorToBlockAdapter implements BlockDevice {
@Override
public DataBuffer readBlock(int block) {
DataBuffer data = DataBuffer.create(STANDARD_BLOCK_SIZE);
operate(block,
(t,s) -> data.put(0, device.readSector(t,s)),
(t,s) -> data.put(256, device.readSector(t,s)));
DataBuffer data = DataBuffer.create(style.blockSize);
operate(block, (t,s,o) -> data.put(o, device.readSector(t,s)));
return data;
}
@Override
public void writeBlock(int block, DataBuffer blockData) {
operate(block,
(t,s) -> device.writeSector(t,s,blockData.slice(0,256)),
(t,s) -> device.writeSector(t,s,blockData.slice(256,256)));
assert blockData.limit() == style.blockSize;
operate(block, (t,s,o) -> device.writeSector(t, s, blockData.slice(o, TrackSectorDevice.SECTOR_SIZE)));
}
public void operate(int block, BiConsumer<Integer,Integer> operation1, BiConsumer<Integer,Integer> operation2) {
int track = block / 8;
int sectorIndex = block % 8;
int[] sectorMapping1 = { 0, 13, 11, 9, 7, 5, 3, 1 };
int[] sectorMapping2 = { 14, 12, 10, 8, 6, 4, 2, 15 };
int sector1 = sectorMapping1[sectorIndex];
int sector2 = sectorMapping2[sectorIndex];
operation1.accept(track, sector1);
operation2.accept(track, sector2);
private void operate(int block, Operation operation) {
assert block < geometry.blocksOnDevice();
int offset = 0;
int physicalSector = block * style.sectorsPerBlock;
int track = physicalSector / device.getGeometry().sectorsPerTrack();
int sector = physicalSector % device.getGeometry().sectorsPerTrack();
while (offset < style.blockSize) {
assert sector < device.getGeometry().sectorsPerTrack();
operation.perform(track, sector, offset);
sector++; // note that we assume we never wrap to next track
offset += TrackSectorDevice.SECTOR_SIZE;
}
}
private interface Operation {
void perform(int track, int sector, int offset);
}
@Override
public void format() {
device.format();
}
public enum BlockStyle {
RDOS(256),
PASCAL(512),
PRODOS(512),
CPM(1024);
final int blockSize;
final int sectorsPerBlock;
BlockStyle(int blockSize) {
assert blockSize % TrackSectorDevice.SECTOR_SIZE == 0;
this.blockSize = blockSize;
this.sectorsPerBlock = blockSize / TrackSectorDevice.SECTOR_SIZE;
}
}
}
@@ -111,7 +111,8 @@ public class DiskWriterTest {
public void testWriteToProdos140kDisk() throws IOException, DiskException {
Source source = DataBufferSource.create(DiskConstants.APPLE_140KB_DISK, "new-disk").get();
TrackSectorDevice trackSectorDevice = new DosOrderedTrackSectorDevice(source);
BlockDevice blockDevice = new TrackSectorToBlockAdapter(trackSectorDevice);
TrackSectorDevice skewedDevice = SkewedTrackSectorDevice.dosToPascalSkew(trackSectorDevice);
BlockDevice blockDevice = new TrackSectorToBlockAdapter(skewedDevice, TrackSectorToBlockAdapter.BlockStyle.PRODOS);
FormattedDisk[] disks = ProdosFormatDisk.create(
"write-test-prodos-140k.dsk", "TEST", blockDevice); //$NON-NLS-1$ //$NON-NLS-2$
writeFiles(disks, "BIN", "TXT", true); //$NON-NLS-1$ //$NON-NLS-2$
@@ -218,7 +219,8 @@ public class DiskWriterTest {
public void testCreateAndDeleteProdos140kDisk() throws IOException, DiskException {
Source source = DataBufferSource.create(DiskConstants.APPLE_140KB_DISK, "new-disk").get();
TrackSectorDevice trackSectorDevice = new DosOrderedTrackSectorDevice(source);
BlockDevice blockDevice = new TrackSectorToBlockAdapter(trackSectorDevice);
TrackSectorDevice skewedDevice = SkewedTrackSectorDevice.dosToPascalSkew(trackSectorDevice);
BlockDevice blockDevice = new TrackSectorToBlockAdapter(skewedDevice, TrackSectorToBlockAdapter.BlockStyle.PRODOS);
FormattedDisk[] disks = ProdosFormatDisk.create(
"createanddelete-test-prodos-140k.dsk", "TEST", //$NON-NLS-1$ //$NON-NLS-2$
blockDevice);
@@ -112,11 +112,12 @@ public class AppleUtilTest {
fileEntry.setFileData("This is a test file.".getBytes()); //$NON-NLS-1$
// A duplicate - then we change it to a NIB disk image...
Source source2 = DataBufferSource.create(DiskConstants.APPLE_140KB_NIBBLE_DISK, "new-disk").get();
TrackSectorDevice tsDevice = new TrackSectorNibbleDevice(new NibbleImage(source2),
TrackSectorDevice nibbleDevice = new TrackSectorNibbleDevice(new NibbleImage(source2),
DiskMarker.disk525sector16(), new Nibble62Disk525Codec(), 16);
TrackSectorDevice skewedDevice = SkewedTrackSectorDevice.physicalToPascalSkew(nibbleDevice);
ProdosFormatDisk prodosDiskNibbleOrder = ProdosFormatDisk.create("prodostemp2.nib", //$NON-NLS-1$
"prodostemp2", //$NON-NLS-1$
new TrackSectorToBlockAdapter(tsDevice))[0];
new TrackSectorToBlockAdapter(skewedDevice, TrackSectorToBlockAdapter.BlockStyle.PRODOS))[0];
AppleUtil.changeOrderByBlock(prodosDiskDosOrder.get(BlockDevice.class).orElseThrow(),
prodosDiskNibbleOrder.get(BlockDevice.class).orElseThrow());
// Confirm that these disks are identical:
@@ -83,8 +83,9 @@ public class NewDeviceTest {
public void readBlockGalatt() {
final String filename = "galatt.dsk";
Source source = sourceDisk(filename);
DosOrderedTrackSectorDevice tsDevice = new DosOrderedTrackSectorDevice(source);
TrackSectorToBlockAdapter blockDevice = new TrackSectorToBlockAdapter(tsDevice);
DosOrderedTrackSectorDevice doDevice = new DosOrderedTrackSectorDevice(source);
TrackSectorDevice skewedDevice = SkewedTrackSectorDevice.dosToPascalSkew(doDevice);
TrackSectorToBlockAdapter blockDevice = new TrackSectorToBlockAdapter(skewedDevice, TrackSectorToBlockAdapter.BlockStyle.PRODOS);
DataBuffer blockData = blockDevice.readBlock(2);
dumpAsHex(blockData, filename);
}