jace/src/main/java/jace/hardware/FloppyDisk.java

421 lines
16 KiB
Java

/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.state.StateManager;
import jace.state.Stateful;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Representation of a 140kb floppy disk image. This also performs conversions as
* needed. Internally, the emulator will always use a "nibblized" disk
* representation during active use. So if any sort of dsk/do/po image is loaded
* it will be converted first. If changes are made to the disk then the tracks
* will be converted back into de-nibblized form prior to saving. The
* DiskIIDrive class managed disk changes, this class is more an interface to
* load/save various disk formats and hold the active disk image while it is in
* use.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class FloppyDisk {
@Stateful
boolean writeProtected;
@Stateful
public int headerLength = 0;
@Stateful
public boolean isNibblizedImage;
@Stateful
public int volumeNumber;
static final public int TRACK_NIBBLE_LENGTH = 0x1A00;
static final public int TRACK_COUNT = 35;
static final public int SECTOR_COUNT = 16;
static final public int HALF_TRACK_COUNT = TRACK_COUNT * 2;
static final public int DISK_NIBBLE_LENGTH = TRACK_NIBBLE_LENGTH * TRACK_COUNT;
static final public int DISK_PLAIN_LENGTH = 143360;
static final public int DISK_2MG_NON_NIB_LENGTH = DISK_PLAIN_LENGTH + 0x040;
static final public int DISK_2MG_NIB_LENGTH = DISK_NIBBLE_LENGTH + 0x040;
@Stateful
public byte[] nibbles = new byte[DISK_NIBBLE_LENGTH];
// Denotes the mapping of physical order (array index) to the dos 3.3 logical order (value)
public static int[] DOS_33_SECTOR_ORDER = {
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F
};
// Denotes the mapping of physical order (array index) to the Prodos logical order (value)
// Borrowed from KEGS -- thanks KEGS team!
public static int[] PRODOS_SECTOR_ORDER = {
0x00, 0x08, 0x01, 0x09, 0x02, 0x0a, 0x03, 0x0b,
0x04, 0x0c, 0x05, 0x0d, 0x06, 0x0e, 0x07, 0x0f
};
// Sector ordering used for current disk
@Stateful
public int[] currentSectorOrder;
// Location of image
@Stateful
public File diskPath;
static int[] NIBBLE_62 = {
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff};
static int[] NIBBLE_62_REVERSE;
static {
NIBBLE_62_REVERSE = new int[256];
for (int i = 0; i < NIBBLE_62.length; i++) {
NIBBLE_62_REVERSE[NIBBLE_62[i] & 0x0ff] = 0x0ff & i;
}
}
private static boolean DEBUG = false;
public FloppyDisk() throws IOException {
// This constructor is only used for disk conversion...
}
/**
*
* @param diskFile
* @throws IOException
*/
public FloppyDisk(File diskFile) throws IOException {
FileInputStream input = new FileInputStream(diskFile);
String name = diskFile.getName().toUpperCase();
readDisk(input, name.endsWith(".PO"));
writeProtected = !diskFile.canWrite();
diskPath = diskFile;
}
// brendanr: refactored to use input stream
public void readDisk(InputStream diskFile, boolean prodosOrder) throws IOException {
isNibblizedImage = true;
volumeNumber = CardDiskII.DEFAULT_VOLUME_NUMBER;
headerLength = 0;
try {
int bytesRead = diskFile.read(nibbles);
if (bytesRead == DISK_2MG_NIB_LENGTH) {
bytesRead -= 0x040;
// Try to pick up volume number from 2MG header.
volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254;
nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length);
headerLength = 0x040;
}
if (bytesRead == DISK_2MG_NON_NIB_LENGTH) {
bytesRead -= 0x040;
// Try to pick up correct sector ordering and volume from 2MG header.
prodosOrder = (nibbles[12] == 01);
volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254;
nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length);
headerLength = 0x040;
}
currentSectorOrder = prodosOrder ? PRODOS_SECTOR_ORDER : DOS_33_SECTOR_ORDER;
if (bytesRead == DISK_PLAIN_LENGTH) {
isNibblizedImage = false;
nibbles = nibblize(nibbles);
if (nibbles.length != DISK_NIBBLE_LENGTH) {
throw new IOException("Nibblized version is wrong size (expected-actual = " + (DISK_NIBBLE_LENGTH - nibbles.length) + ")");
}
} else if (bytesRead != DISK_NIBBLE_LENGTH) {
throw new IOException("Bad NIB size " + bytesRead + "; JACE only recognizes plain images " + DISK_PLAIN_LENGTH + " or nibble images " + DISK_NIBBLE_LENGTH + " sizes");
}
} catch (IOException ex) {
throw ex;
}
StateManager.markDirtyValue(nibbles);
StateManager.markDirtyValue(currentSectorOrder);
}
/*
* Convert a block-format disk to a 6-by-2 nibblized encoding scheme (raw NIB disk format)
*/
public byte[] nibblize(byte[] nibbles) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (int track = 0; track < TRACK_COUNT; track++) {
for (int sector = 0; sector < SECTOR_COUNT; sector++) {
// 15 junk bytes
writeJunkBytes(output, 15);
// Address block
writeAddressBlock(output, track, sector);
// 4 junk bytes
writeJunkBytes(output, 4);
// Data block
nibblizeBlock(output, track, currentSectorOrder[sector], nibbles);
// 34 junk bytes
writeJunkBytes(output, 34);
}
}
return output.toByteArray();
}
private void writeJunkBytes(ByteArrayOutputStream output, int i) {
for (int b = 0; b < i; b++) {
output.write(0x0FF);
}
}
private void writeAddressBlock(ByteArrayOutputStream output, int track, int sector) throws IOException {
output.write(0x0d5);
output.write(0x0aa);
output.write(0x096);
int checksum = 00;
// volume
checksum ^= volumeNumber;
output.write(getOddEven(volumeNumber));
// track
checksum ^= track;
output.write(getOddEven(track));
// sector
checksum ^= sector;
output.write(getOddEven(sector));
// checksum
output.write(getOddEven(checksum & 0x0ff));
output.write(0x0de);
output.write(0x0aa);
output.write(0x0eb);
}
private byte[] getOddEven(int i) {
byte[] out = new byte[2];
out[0] = (byte) (0xAA | (i >> 1));
out[1] = (byte) (0xAA | i);
return out;
}
private int decodeOddEven(byte b1, byte b2) {
// return (((b1 ^ 0x0AA) << 1) & 0x0ff) | ((b2 ^ 0x0AA) & 0x0ff);
int result = ((((b1 << 1) | 1) & b2) & 0x0ff);
return result;
}
private void nibblizeBlock(ByteArrayOutputStream output, int track, int sector, byte[] nibbles) {
int offset = ((track * SECTOR_COUNT) + sector) * 256;
int[] temp = new int[342];
for (int i = 0; i < 256; i++) {
temp[i] = (nibbles[offset + i] & 0x0ff) >> 2;
}
int hi = 0x001;
int med = 0x0AB;
int low = 0x055;
for (int i = 0; i < 0x56; i++) {
int value = ((nibbles[offset + hi] & 1) << 5)
| ((nibbles[offset + hi] & 2) << 3)
| ((nibbles[offset + med] & 1) << 3)
| ((nibbles[offset + med] & 2) << 1)
| ((nibbles[offset + low] & 1) << 1)
| ((nibbles[offset + low] & 2) >> 1);
temp[i + 256] = value;
hi = (hi - 1) & 0x0ff;
med = (med - 1) & 0x0ff;
low = (low - 1) & 0x0ff;
}
output.write(0x0d5);
output.write(0x0aa);
output.write(0x0ad);
int last = 0;
for (int i = temp.length - 1; i > 255; i--) {
int value = temp[i] ^ last;
output.write(NIBBLE_62[value]);
last = temp[i];
}
for (int i = 0; i < 256; i++) {
int value = temp[i] ^ last;
output.write(NIBBLE_62[value]);
last = temp[i];
}
// Last data byte used as checksum
output.write(NIBBLE_62[last]);
output.write(0x0de);
output.write(0x0aa);
output.write(0x0eb);
}
public void updateTrack(Integer track) {
// If disk is nibble image, write nibbles directly
if (isNibblizedImage) {
updateNibblizedTrack(track);
}
// Otherwise denibblize and write out
if (!isNibblizedImage) {
updateDenibblizedTrack(track);
}
}
void updateNibblizedTrack(Integer track) {
try {
RandomAccessFile disk = new RandomAccessFile(diskPath, "rws");
// Locate start of track
disk.seek(headerLength + track * TRACK_NIBBLE_LENGTH);
// Update that section of the disk image
disk.write(nibbles, track * TRACK_NIBBLE_LENGTH, TRACK_NIBBLE_LENGTH);
disk.close();
} catch (FileNotFoundException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
static public boolean CHECK_NIB_SECTOR_PATTERN_ON_WRITE = true;
void updateDenibblizedTrack(Integer track) {
try {
byte[] trackNibbles = new byte[TRACK_NIBBLE_LENGTH];
byte[] trackData = new byte[SECTOR_COUNT * 256];
// Copy track into temporary buffer
// System.out.println("Nibblized track "+track);
// System.out.printf("%04d:",0);
for (int i = 0, pos = track * TRACK_NIBBLE_LENGTH; i < TRACK_NIBBLE_LENGTH; i++, pos++) {
trackNibbles[i] = nibbles[pos];
// System.out.print(Integer.toString(nibbles[pos] & 0x0ff, 16)+" ");
// if (i % 16 == 15) {
// System.out.println();
// System.out.printf("%04d:",i+1);
// }
}
// System.out.println();
int pos = 0;
for (int i = 0; i < SECTOR_COUNT; i++) {
// Loop through number of sectors
pos = locatePattern(pos, trackNibbles, 0x0d5, 0x0aa, 0x096);
// Locate track number
int trackVerify = decodeOddEven(trackNibbles[pos + 5], trackNibbles[pos + 6]);
// Locate sector number
int sector = decodeOddEven(trackNibbles[pos + 7], trackNibbles[pos + 8]);
// System.out.println("Writing track " + track + ", getting address block for T" + trackVerify + ".S" + sector + " found at NIB offset "+pos);
// Skip to end of address block
pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa /*, 0x0eb this is sometimes being written as FF??*/);
// Locate start of sector data
pos = locatePattern(pos, trackNibbles, 0x0d5, 0x0aa, 0x0ad);
// Determine offset in output data for sector
//int offset = reverseLoopkup(currentSectorOrder, sector) * 256;
int offset = currentSectorOrder[sector] * 256;
// System.out.println("Sector "+sector+" maps to physical sector "+reverseLoopkup(currentSectorOrder, sector));
// Decode sector data
denibblizeSector(trackNibbles, pos + 3, trackData, offset);
// Skip to end of sector
pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa, 0x0eb);
}
// Write track to disk
RandomAccessFile disk;
try {
disk = new RandomAccessFile(diskPath, "rws");
disk.seek(headerLength + track * 256 * SECTOR_COUNT);
disk.write(trackData);
disk.close();
} catch (FileNotFoundException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (Throwable ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
private int locatePattern(int pos, byte[] data, int... pattern) throws Throwable {
int max = data.length;
while (!matchPattern(pos, data, pattern)) {
pos = (pos + 1) % data.length;
max--;
if (max < 0) {
throw new Throwable("Could not match pattern!");
}
}
// System.out.print("Found pattern at "+pos+": ");
// for (int i : pattern) {System.out.print(Integer.toString( i & 0x0ff, 16)+" ");}
// System.out.println();
return pos;
}
private boolean matchPattern(int pos, byte[] data, int... pattern) {
int matched = 0;
for (int i : pattern) {
int d = data[pos] & 0x0ff;
if (d != i) {
if (matched > 1) {
System.out.println("Warning: Issue when interpreting nibbilized disk data: at position " + pos + " pattern byte " + Integer.toString(i, 16) + " doesn't match " + Integer.toString(d, 16));
}
return false;
}
pos = (pos + 1) % data.length;
matched++;
}
return true;
}
private void denibblizeSector(byte[] source, int pos, byte[] trackData, int offset) {
int[] temp = new int[342];
int current = pos;
int last = 0;
// Un-encode raw data, leaving with pre-nibblized bytes
for (int i = temp.length - 1; i > 255; i--) {
int t = NIBBLE_62_REVERSE[0x0ff & source[current++]];
temp[i] = t ^ last;
last ^= t;
}
for (int i = 0; i < 256; i++) {
int t = NIBBLE_62_REVERSE[0x0ff & source[current++]];
temp[i] = t ^ last;
last ^= t;
}
// Now decode the pre-nibblized bytes
int p = temp.length - 1;
for (int i = 0; i < 256; i++) {
int a = (temp[i] << 2);
a = a + ((temp[p] & 1) << 1) + ((temp[p] & 2) >> 1);
trackData[i + offset] = (byte) a;
temp[p] = temp[p] >> 2;
p--;
if (p < 256) {
p = temp.length - 1;
}
}
}
private int reverseLoopkup(int[] table, int value) {
for (int i = 0; i < table.length; i++) {
if (table[i] == value) {
return i;
}
}
return -1;
}
}