Switching CP/M over to use a BlockDevice.

This commit is contained in:
Rob Greene
2025-08-31 18:45:50 -05:00
parent 696149056f
commit 4ae852fc48
9 changed files with 123 additions and 110 deletions
@@ -429,12 +429,12 @@ public class ScanCommand extends ReusableCommandOptions {
private void readAllCPMBlocks(CpmFormatDisk cpm) {
dataType = "CPM blocks";
// This adjusts for the start. The CPM filesystem ignores the first 3 tracks on disk.
int blocksToRead = cpm.getBitmapLength() -
(CpmFormatDisk.PHYSICAL_BLOCK_TRACK_START * CpmFormatDisk.CPM_BLOCKS_PER_TRACK);
for (int b=0; b<blocksToRead && errors.size() < MAX_ERRORS; b++) {
// Note that the "raw" device can read the entire CP/M disk and that the CP/M filesystem handles the
// "logical" block 0 starting on track 3.
BlockDevice device = cpm.get(BlockDevice.class).orElseThrow();
for (int b = 0; b < device.getGeometry().blocksOnDevice(); b++) {
try {
cpm.readCpmBlock(b);
device.readBlock(b);
dataRead++;
} catch (Throwable t) {
errors.add(String.format("Unable to read CPM block #%d for disk #%d", b, cpm.getLogicalDiskNumber()));
@@ -21,8 +21,13 @@ package com.webcodepro.applecommander.storage.os.cpm;
import com.webcodepro.applecommander.storage.DiskConstants;
import com.webcodepro.applecommander.storage.DiskFactory;
import org.applecommander.device.*;
import org.applecommander.hint.Hint;
import org.applecommander.util.DataBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Test this disk for a likely CP/M filesystem.
* @see <a href="https://www.seasip.info/Cpm/format22.html">CP/M 2.2</a>
@@ -31,9 +36,37 @@ import org.applecommander.util.DataBuffer;
public class CpmDiskFactory implements DiskFactory {
@Override
public void inspect(Context ctx) {
ctx.orders.forEach(order -> {
if (order.isSizeApprox(DiskConstants.APPLE_140KB_DISK) || order.isSizeApprox(DiskConstants.APPLE_140KB_NIBBLE_DISK)) {
CpmFormatDisk disk = new CpmFormatDisk(ctx.source.getName(), order);
List<TrackSectorDevice> devices = new ArrayList<>();
if (ctx.sectorDevice != null) {
if (ctx.sectorDevice.is(Hint.NIBBLE_SECTOR_ORDER)) {
// cheating so I don't need to figure out physical to CP/M skew!
TrackSectorDevice dosSkew = SkewedTrackSectorDevice.physicalToDosSkew(ctx.sectorDevice);
devices.add(SkewedTrackSectorDevice.dosToCpmSkew(dosSkew));
}
else if (ctx.sectorDevice.is(Hint.DOS_SECTOR_ORDER)) {
devices.add(SkewedTrackSectorDevice.dosToCpmSkew(ctx.sectorDevice));
}
else if (ctx.sectorDevice.is(Hint.PRODOS_BLOCK_ORDER)) {
devices.add(SkewedTrackSectorDevice.pascalToCpmSkew(ctx.sectorDevice));
}
else {
// Presumably a DSK image, so DO and PO are possibilities
devices.add(SkewedTrackSectorDevice.dosToCpmSkew(ctx.sectorDevice));
devices.add(SkewedTrackSectorDevice.pascalToCpmSkew(ctx.sectorDevice));
}
}
else if (ctx.blockDevice != null) {
if (ctx.blockDevice.getGeometry().blocksOnDevice() == 280) {
TrackSectorDevice device = new BlockToTrackSectorAdapter(ctx.blockDevice,
new ProdosBlockToTrackSectorAdapterStrategy());
devices.add(SkewedTrackSectorDevice.pascalToCpmSkew(device));
}
}
// Any devices in the list are expected to be in CP/M block order
devices.forEach(device -> {
if (device.getGeometry().sectorsPerDisk() == 560) {
BlockDevice blockDevice = new TrackSectorToBlockAdapter(device, TrackSectorToBlockAdapter.BlockStyle.CPM);
CpmFormatDisk disk = new CpmFormatDisk(ctx.source.getName(), blockDevice);
if (check(disk)) {
ctx.disks.add(disk);
}
@@ -53,7 +86,7 @@ public class CpmDiskFactory implements DiskFactory {
if (e5count != CpmFileEntry.ENTRY_LENGTH) { // Not all bytes were 0xE5
// Check user number. Should be 0-15 or 0xE5
int userNumber = entries.getUnsignedByte(offset);
if (userNumber > 15 && userNumber != 0xe5) return false;
if (userNumber > 0x1f && userNumber != 0xe5) return false;
// Validate filename has highbit off and is a character
for (int i=0; i<8; i++) {
int ch = entries.getUnsignedByte(offset+1+i);
@@ -502,6 +502,10 @@ public class CpmFileEntry implements FileEntry {
* Answer with a list of blocks allocated to this file.
*/
public int[] getAllocations() {
// It appears that a user number of 0x1f ("cp/m.sys", "DOS 3.3.") marks system tracks!
// Presumably an unmatched user number (0x00 likely being the default) hides the "file".
boolean adjust = getUserNumber(0) == 0x1f;
int blocks = getBlocksUsed();
int[] allocations = new int[blocks];
int block = 0;
@@ -509,9 +513,11 @@ public class CpmFileEntry implements FileEntry {
byte[] data = readFileEntry(i);
int offset = ALLOCATION_OFFSET;
while (block < blocks && offset < ENTRY_LENGTH) {
allocations[block++] = AppleUtil.getUnsignedByte(data[offset++]);
int allocation = AppleUtil.getUnsignedByte(data[offset++]);
if (adjust && allocation >= 0x80 && allocation <= 0x8b) allocation &= 0x7f;
allocations[block++] = allocation;
}
}
return allocations;
return allocations;
}
}
@@ -20,10 +20,11 @@
package com.webcodepro.applecommander.storage.os.cpm;
import com.webcodepro.applecommander.storage.*;
import com.webcodepro.applecommander.storage.physical.ImageOrder;
import com.webcodepro.applecommander.util.AppleUtil;
import com.webcodepro.applecommander.util.TextBundle;
import static com.webcodepro.applecommander.storage.DiskConstants.*;
import org.applecommander.device.BlockDevice;
import org.applecommander.source.Source;
import org.applecommander.util.Container;
import org.applecommander.util.DataBuffer;
import java.util.*;
@@ -32,7 +33,7 @@ import java.util.*;
* <p>
* @author Rob Greene
*/
public class CpmFormatDisk extends FormattedDiskX {
public class CpmFormatDisk extends FormattedDisk implements Container {
private TextBundle textBundle = StorageBundle.getInstance();
/**
* The size of the CP/M sector. Assumed to be 128.
@@ -60,12 +61,10 @@ public class CpmFormatDisk extends FormattedDiskX {
* (The other tracks are boot-related and not available.)
*/
public static final int PHYSICAL_BLOCK_TRACK_START = 3;
/**
* The sector skew of the CP/M disk image.
*/
public static final int[] sectorSkew = {
0x0, 0x6, 0xc, 0x3, 0x9, 0xf, 0xe, 0x5,
0xb, 0x2, 0x8, 0x7, 0xd, 0x4, 0xa, 0x1 };
/**
* The underlying block number which CP/M data block #0 resides.
*/
public static final int FIRST_DATA_BLOCK = PHYSICAL_BLOCK_TRACK_START * PHYSICAL_SECTORS_PER_BLOCK;
/**
* Manage CP/M disk usage.
@@ -90,24 +89,32 @@ public class CpmFormatDisk extends FormattedDiskX {
}
}
private BlockDevice device;
/**
* Construct a CP/M formatted disk.
*/
public CpmFormatDisk(String filename, ImageOrder imageOrder) {
super(filename, imageOrder);
public CpmFormatDisk(String filename, BlockDevice device) {
super(filename, device.get(Source.class).orElseThrow());
this.device = device;
}
/**
* Create a CpmFormatDisk. All CP/M disk images are expected to
* be 140K in size.
*/
public static CpmFormatDisk[] create(String filename, ImageOrder imageOrder) {
CpmFormatDisk disk = new CpmFormatDisk(filename, imageOrder);
public static CpmFormatDisk[] create(String filename, BlockDevice device) {
CpmFormatDisk disk = new CpmFormatDisk(filename, device);
disk.format();
return new CpmFormatDisk[] { disk };
}
/**
@Override
public <T> Optional<T> get(Class<T> iface) {
return Container.get(iface, device);
}
/**
* There apparently is no corresponding CP/M disk name.
* @see com.webcodepro.applecommander.storage.FormattedDisk#getDiskName()
*/
@@ -128,7 +135,7 @@ public class CpmFormatDisk extends FormattedDiskX {
* @see com.webcodepro.applecommander.storage.FormattedDisk#getFreeSpace()
*/
public int getFreeSpace() {
return getPhysicalSize() - getUsedSpace();
return device.getGeometry().deviceSize() - getUsedSpace();
}
/**
@@ -170,7 +177,7 @@ public class CpmFormatDisk extends FormattedDiskX {
* @see com.webcodepro.applecommander.storage.FormattedDisk#getBitmapLength()
*/
public int getBitmapLength() {
return getPhysicalSize() / CPM_BLOCKSIZE;
return device.getGeometry().deviceSize() / CPM_BLOCKSIZE;
}
/**
@@ -258,7 +265,7 @@ public class CpmFormatDisk extends FormattedDiskX {
for (int i=0; i<allocations.length; i++) {
int blockNumber = allocations[i];
if (blockNumber > 0) {
byte[] block = readCpmBlock(blockNumber);
byte[] block = device.readBlock(FIRST_DATA_BLOCK+blockNumber).asBytes();
System.arraycopy(block, 0,
data, i * CPM_BLOCKSIZE, CPM_BLOCKSIZE);
}
@@ -275,16 +282,12 @@ public class CpmFormatDisk extends FormattedDiskX {
* @see com.webcodepro.applecommander.storage.FormattedDisk#format()
*/
public void format() {
getImageOrder().format();
byte[] sectorData = new byte[SECTOR_SIZE];
for (int i=0; i<SECTOR_SIZE; i++) {
sectorData[i] = (byte) 0xe5;
}
for (int track=0; track<35; track++) {
for (int sector=0; sector<16; sector++) {
writeSector(track, sector, sectorData);
}
}
device.format();
DataBuffer blockData = DataBuffer.create(CPM_BLOCKSIZE);
blockData.fill(0xe5);
for (int block=0; block<device.getGeometry().blocksOnDevice(); block++) {
device.writeBlock(block, blockData);
}
}
/**
@@ -421,65 +424,18 @@ public class CpmFormatDisk extends FormattedDiskX {
public boolean canCreateFile() {
return false;
}
/**
* Read a CP/M block (1K in size).
*/
public byte[] readCpmBlock(int block) {
byte[] data = new byte[CPM_BLOCKSIZE];
int track = computeTrack(block);
int sector = computeSector(block);
for (int i=0; i<PHYSICAL_SECTORS_PER_BLOCK; i++) {
System.arraycopy(readSector(track, sectorSkew[sector+i]),
0, data, i*SECTOR_SIZE, SECTOR_SIZE);
}
return data;
}
public byte[] readCpmFileEntries() {
byte[] data = new byte[2 * CpmFormatDisk.CPM_BLOCKSIZE];
System.arraycopy(readCpmBlock(0), 0, data,
0, CpmFormatDisk.CPM_BLOCKSIZE);
System.arraycopy(readCpmBlock(1), 0, data,
CpmFormatDisk.CPM_BLOCKSIZE, CpmFormatDisk.CPM_BLOCKSIZE);
return data;
DataBuffer directory = DataBuffer.create(2 * CPM_BLOCKSIZE);
directory.put(0, device.readBlock(FIRST_DATA_BLOCK));
directory.put(CPM_BLOCKSIZE, device.readBlock(FIRST_DATA_BLOCK+1));
return directory.asBytes();
}
public void writeCpmFileEntries(byte[] data) {
byte[] block = new byte[CPM_BLOCKSIZE];
System.arraycopy(data, 0, block,
0, CpmFormatDisk.CPM_BLOCKSIZE);
writeCpmBlock(0, block);
System.arraycopy(data, CpmFormatDisk.CPM_BLOCKSIZE, block,
0, CpmFormatDisk.CPM_BLOCKSIZE);
writeCpmBlock(1, block);
}
/**
* Compute the physical track number.
*/
protected int computeTrack(int block) {
return PHYSICAL_BLOCK_TRACK_START + (block / CPM_BLOCKS_PER_TRACK);
}
/**
* Compute the physical sector number. The rest of the block
* follows in sequential order.
*/
protected int computeSector(int block) {
return (block % CPM_BLOCKS_PER_TRACK) * PHYSICAL_SECTORS_PER_BLOCK;
}
/**
* Write a CP/M block.
*/
public void writeCpmBlock(int block, byte[] data) {
int track = computeTrack(block);
int sector = computeSector(block);
byte[] sectorData = new byte[SECTOR_SIZE];
for (int i=0; i<PHYSICAL_SECTORS_PER_BLOCK; i++) {
System.arraycopy(data, i*SECTOR_SIZE, sectorData, 0, SECTOR_SIZE);
writeSector(track, sectorSkew[sector+i], sectorData);
}
assert data.length == 2*CPM_BLOCKSIZE;
DataBuffer directory = DataBuffer.wrap(data);
device.writeBlock(FIRST_DATA_BLOCK, directory.slice(0, CPM_BLOCKSIZE));
device.writeBlock(FIRST_DATA_BLOCK+1, directory.slice(CPM_BLOCKSIZE, CPM_BLOCKSIZE));
}
/**
@@ -523,17 +479,6 @@ public class CpmFormatDisk extends FormattedDiskX {
return true;
}
/**
* Change to a different ImageOrder. Remains in CP/M format but
* the underlying order can change.
* @see ImageOrder
*/
public void changeImageOrder(ImageOrder imageOrder) {
AppleUtil.changeImageOrderByTrackAndSector(getImageOrder(), imageOrder);
setImageOrder(imageOrder);
}
/**
* Writes the raw bytes into the file. This bypasses any special formatting
* of the data (such as prepending the data with a length and/or an address).
@@ -24,6 +24,7 @@ import org.applecommander.hint.Hint;
import org.applecommander.util.Container;
import org.applecommander.util.DataBuffer;
import javax.sound.midi.Track;
import java.util.Optional;
/**
@@ -83,9 +84,10 @@ public class SkewedTrackSectorDevice implements TrackSectorDevice {
}
// 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);
return new SkewedTrackSectorDevice(device, 0,6,12,3,9,15,14,5,11,2,8,7,13,4,10,1);
}
public static TrackSectorDevice pascalToCpmSkew(TrackSectorDevice device) {
return new SkewedTrackSectorDevice(device, 0,9,3,12,6,15,1,10,4,13,7,8,2,11,5,14);
}
// Special RDOS "skew" for truncation (from 16 sector to 13 sector)
public static TrackSectorDevice truncate16sectorTo13(TrackSectorDevice device) {
@@ -227,6 +227,20 @@ public class DiskHelperTest {
assertCanReadFiles(disks);
}
@Test
public void testCPMV233Disk() throws DiskException, IOException {
FormattedDisk[] disks = showDirectory(config.getDiskDir() +
"/CPMV233.DSK");
assertCanReadFiles(disks);
}
@Test
public void testCPAM51BDisk() throws DiskException, IOException {
FormattedDisk[] disks = showDirectory(config.getDiskDir() +
"/CPAM51B.dsk");
assertCanReadFiles(disks);
}
protected FormattedDisk[] showDirectory(String imageName) throws IOException, DiskException {
Source source = Sources.create(imageName).orElseThrow();
DiskFactory.Context ctx = Disks.inspect(source);
Binary file not shown.
Binary file not shown.
@@ -19,6 +19,7 @@
*/
package com.webcodepro.applecommander.ui.swt.wizard.diskimage;
import com.webcodepro.applecommander.ui.swt.util.SwtUtil;
import org.applecommander.codec.Nibble62Disk525Codec;
import org.applecommander.codec.NibbleDiskCodec;
import org.applecommander.device.*;
@@ -123,6 +124,9 @@ public class DiskImageWizard extends Wizard {
blockDevice = new ProdosOrderedBlockDevice(source, BlockDevice.STANDARD_BLOCK_SIZE);
sectorDevice = new BlockToTrackSectorAdapter(blockDevice, new ProdosBlockToTrackSectorAdapterStrategy());
break;
default:
SwtUtil.showErrorDialog(getDialog(), "Bug!", "Unexpected order value: " + getOrder());
return null;
}
switch (format) {
case FORMAT_DOS33:
@@ -139,7 +143,16 @@ public class DiskImageWizard extends Wizard {
case FORMAT_UNIDOS:
return UniDosFormatDisk.create(name.toString(), imageOrder);
case FORMAT_CPM:
return CpmFormatDisk.create(name.toString(), imageOrder);
TrackSectorDevice cpmDevice = switch (getOrder()) {
case ORDER_DOS -> SkewedTrackSectorDevice.dosToCpmSkew(sectorDevice);
case ORDER_NIBBLE -> SkewedTrackSectorDevice.dosToCpmSkew(SkewedTrackSectorDevice.physicalToDosSkew(sectorDevice));
case ORDER_PRODOS -> SkewedTrackSectorDevice.pascalToCpmSkew(sectorDevice);
default -> null;
};
if (cpmDevice != null) {
BlockDevice cpmBlock = new TrackSectorToBlockAdapter(cpmDevice, TrackSectorToBlockAdapter.BlockStyle.CPM);
return CpmFormatDisk.create(name.toString(), cpmBlock);
}
}
return null;
}