twoapple-reboot/src/peripheral/diskii.d

1145 lines
30 KiB
D

/+
+ peripheral/diskii.d
+
+ Copyright: 2007 Gerald Stocker
+
+ This file is part of Twoapple.
+
+ Twoapple is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ Twoapple 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Twoapple; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+/
module peripheral.diskii;
import peripheral.base;
import device.base;
import memory;
import timer;
import ui.mainwindow;
import ui.sound;
import std.stream;
ubyte[256] controllerRom = [
0xa2, 0x20, 0xa0, 0x00, 0xa2, 0x03, 0x86, 0x3c,
0x8a, 0x0a, 0x24, 0x3c, 0xf0, 0x10, 0x05, 0x3c,
0x49, 0xff, 0x29, 0x7e, 0xb0, 0x08, 0x4a, 0xd0,
0xfb, 0x98, 0x9d, 0x56, 0x03, 0xc8, 0xe8, 0x10,
0xe5, 0x20, 0x58, 0xff, 0xba, 0xbd, 0x00, 0x01,
0x0a, 0x0a, 0x0a, 0x0a, 0x85, 0x2b, 0xaa, 0xbd,
0x8e, 0xc0, 0xbd, 0x8c, 0xc0, 0xbd, 0x8a, 0xc0,
0xbd, 0x89, 0xc0, 0xa0, 0x50, 0xbd, 0x80, 0xc0,
0x98, 0x29, 0x03, 0x0a, 0x05, 0x2b, 0xaa, 0xbd,
0x81, 0xc0, 0xa9, 0x56, 0x20, 0xa8, 0xfc, 0x88,
0x10, 0xeb, 0x85, 0x26, 0x85, 0x3d, 0x85, 0x41,
0xa9, 0x08, 0x85, 0x27, 0x18, 0x08, 0xbd, 0x8c,
0xc0, 0x10, 0xfb, 0x49, 0xd5, 0xd0, 0xf7, 0xbd,
0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xf3,
0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0x96,
0xf0, 0x09, 0x28, 0x90, 0xdf, 0x49, 0xad, 0xf0,
0x25, 0xd0, 0xd9, 0xa0, 0x03, 0x85, 0x40, 0xbd,
0x8c, 0xc0, 0x10, 0xfb, 0x2a, 0x85, 0x3c, 0xbd,
0x8c, 0xc0, 0x10, 0xfb, 0x25, 0x3c, 0x88, 0xd0,
0xec, 0x28, 0xc5, 0x3d, 0xd0, 0xbe, 0xa5, 0x40,
0xc5, 0x41, 0xd0, 0xb8, 0xb0, 0xb7, 0xa0, 0x56,
0x84, 0x3c, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0x59,
0xd6, 0x02, 0xa4, 0x3c, 0x88, 0x99, 0x00, 0x03,
0xd0, 0xee, 0x84, 0x3c, 0xbc, 0x8c, 0xc0, 0x10,
0xfb, 0x59, 0xd6, 0x02, 0xa4, 0x3c, 0x91, 0x26,
0xc8, 0xd0, 0xef, 0xbc, 0x8c, 0xc0, 0x10, 0xfb,
0x59, 0xd6, 0x02, 0xd0, 0x87, 0xa0, 0x00, 0xa2,
0x56, 0xca, 0x30, 0xfb, 0xb1, 0x26, 0x5e, 0x00,
0x03, 0x2a, 0x5e, 0x00, 0x03, 0x2a, 0x91, 0x26,
0xc8, 0xd0, 0xee, 0xe6, 0x27, 0xe6, 0x3d, 0xa5,
0x3d, 0xcd, 0x00, 0x08, 0xa6, 0x2b, 0x90, 0xdb,
0x4c, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00
];
class StopTimer
{
Timer timer;
Timer.Counter stopCounter;
void delegate() notifyExpired;
void startCountdown()
{
if (stopCounter is null)
stopCounter = timer.new Counter(1_020_484, &expire);
}
void stopCountdown()
{
if (stopCounter !is null)
{
stopCounter.discard();
stopCounter = null;
}
}
bool expire()
{
notifyExpired();
stopCounter = null;
return false;
}
}
class Controller : Peripheral
{
import gtk.VBox;
Drive[2] drives;
int activeDrive;
bool writeMode;
bool loadRegister;
bool delegate() checkFinalCycle;
ubyte dataLatch;
bool isOn;
StopTimer drivesOffDelay;
this()
{
ioSelectROM = controllerRom;
statusBox = new VBox(false, 3);
drives[0] = new Drive(1);
statusBox.packStart(drives[0].status.display, false, false, 0);
drives[1] = new Drive(2);
statusBox.packStart(drives[1].status.display, false, false, 0);
}
void reset()
{
drives[0].deactivate();
drives[1].deactivate();
activeDrive = 0;
}
void initTimer(Timer timer)
{
drivesOffDelay = new StopTimer();
drivesOffDelay.timer = timer;
drivesOffDelay.notifyExpired = &doDrivesOff;
}
void doDrivesOff()
{
drives[activeDrive].deactivate();
isOn = false;
}
void changeActiveDrive(int newActive)
{
drives[activeDrive].deactivate();
if (isOn) drives[newActive].activate();
activeDrive = newActive;
}
ubyte checkWriteProtect()
{
if (drives[activeDrive].writeProtected ||
drives[activeDrive].magnets[1])
return 0x80;
else
return 0x00;
}
ubyte phaseOff(ushort addr)
{
int phase = (addr >> 1) & 3;
drives[activeDrive].setPhase(phase, false);
return 0xFF;
}
void phaseOn(ushort addr)
{
int phase = (addr >> 1) & 3;
drives[activeDrive].setPhase(phase, true);
}
ubyte drive_0_select()
{
if (activeDrive != 0) changeActiveDrive(0);
return 0xFF;
}
void drive_1_select()
{
if (activeDrive != 1) changeActiveDrive(1);
}
ubyte drivesOff()
{
drivesOffDelay.startCountdown();
return 0xFF;
}
void drivesOn()
{
isOn = true;
drivesOffDelay.stopCountdown();
drives[activeDrive].activate();
}
void readQ6H()
{
loadRegister = true;
}
ubyte Q6L()
{
loadRegister = false;
if (isOn && checkFinalCycle())
{
if (writeMode)
{
drives[activeDrive].write(dataLatch);
return dataLatch;
}
else
{
return drives[activeDrive].read();
}
}
return 0x00;
}
void writeQ6H(ubyte val)
{
loadRegister = true;
dataLatch = val;
}
ubyte Q7L()
{
writeMode = false;
if (loadRegister) return checkWriteProtect();
else return 0xFF;
}
void readQ7H()
{
writeMode = true;
}
void writeQ7H(ubyte val)
{
writeMode = true;
dataLatch = val;
}
mixin(InitSwitches("", [
mixin(MakeSwitch([0xC080, 0xC082, 0xC084, 0xC086],
"RW", "phaseOff(addr)")),
mixin(MakeSwitch([0xC081, 0xC083, 0xC085, 0xC087],
"R0W", "phaseOn(addr)")),
mixin(MakeSwitch([0xC088], "RW", "drivesOff")),
mixin(MakeSwitch([0xC089], "R0W", "drivesOn")),
mixin(MakeSwitch([0xC08A], "RW", "drive_0_select")),
mixin(MakeSwitch([0xC08B], "R0W", "drive_1_select")),
mixin(MakeSwitch([0xC08C], "RW", "Q6L")),
mixin(MakeSwitch([0xC08D], "R0", "readQ6H")),
mixin(MakeSwitch([0xC08D], "W", "writeQ6H(val)")),
mixin(MakeSwitch([0xC08E], "RW", "Q7L")),
mixin(MakeSwitch([0xC08F], "R0", "readQ7H")),
mixin(MakeSwitch([0xC08F], "W", "writeQ7H(val)"))
]));
}
class DriveStatus
{
import gtk.HBox;
import gtk.MenuBar;
import gtk.MenuItem;
import gtk.ProgressBar;
import gtk.CheckButton;
import gtk.ToggleButton;
import gtk.Label;
import gtkc.pangotypes;
import std.string;
Drive drive;
HBox display;
ProgressBar activity;
string statusClean, statusDirty;
CheckButton wpButton;
Label imgName;
MenuItem openItem;
MenuItem saveItem;
MenuItem ejectItem;
this(Drive drive_, int driveNum)
{
drive = drive_;
statusClean = "Drive " ~ std.string.toString(driveNum);
statusDirty = "( " ~ statusClean ~ " )";
activity = new ProgressBar();
activity.setText(statusClean);
auto menuBar = new MenuBar();
auto menu = menuBar.append("Image");
openItem = new MenuItem(&onOpenImage, "Open", "img.open", false);
saveItem = new MenuItem(&onSaveImage, "Save", "img.save", false);
ejectItem = new MenuItem(&onEjectImage, "Eject", "img.eject", false);
menu.append(openItem);
menu.append(saveItem);
menu.append(ejectItem);
saveItem.setSensitive(false);
ejectItem.setSensitive(false);
wpButton = new CheckButton("WP", false);
wpButton.addOnToggled(&onWPToggle);
wpButton.setSensitive(false);
imgName = new Label("No disk");
imgName.setMaxWidthChars(35);
imgName.setEllipsize(PangoEllipsizeMode.START);
display = new HBox(false, 3);
display.packStart(activity, false, false, 0);
display.packStart(wpButton, false, false, 0);
display.packStart(imgName, true, true, 0);
display.packStart(menuBar, false, false, 0);
}
void setActive(bool isActive)
{
soundCard.speedyMode = isActive;
activity.setFraction(isActive ? 1.0 : 0.0);
}
void setDirty(bool isDirty)
{
if (isDirty)
activity.setText(statusDirty);
else
activity.setText(statusClean);
saveItem.setSensitive(isDirty);
}
void onWPToggle(ToggleButton b)
{
drive.writeProtected = (b.getActive() != 0);
}
void onOpenImage(MenuItem m)
{
TwoappleFile file = TwoappleFilePicker.open("image file",
&ExternalImage.isValidImage);
if (file is null) return;
if (!drive.loadImage(file)) return;
if (!file.canWrite())
{
wpButton.setActive(true);
wpButton.setSensitive(false);
}
else
wpButton.setSensitive(true);
setDirty(false);
ejectItem.setSensitive(true);
imgName.setText(file.fileName);
}
void onEjectImage(MenuItem m)
{
if (!drive.ejectImage()) return;
setDirty(false);
wpButton.setActive(false);
wpButton.setSensitive(true);
ejectItem.setSensitive(false);
imgName.setText("No disk");
}
void onSaveImage(MenuItem m)
{
if (!drive.saveImage()) return;
setDirty(false);
}
}
class Drive
{
InternalImage imgData;
ExternalImage imgFile;
DriveStatus status;
bool writeProtected;
int headPos, maxHeadPos, track;
bool[4] magnets;
this(int driveNum)
{
maxHeadPos = (InternalImage.NUM_TRACKS - 1) * 4;
imgData = new NonImage();
status = new DriveStatus(this, driveNum);
}
void setPhase(int phase, bool newState)
{
//if (magnets[phase] == newState) return;
magnets[phase] = newState;
// Find the number of active magnets and their
// distances from any cog.
int totalOn = 0, delta;
for (int p = 0; p < 4; ++p)
{
if (magnets[p])
{
++totalOn;
delta = (p * 2) - (headPos % 8);
}
}
// Do not move the head if more than one magnet
// is on (which precludes quarter-tracking).
if (totalOn != 1) return;
// Do not move the head if the active magnet is
// equidistant from the two nearest cogs, or if
// it is already in line with a cog.
if ((delta == -4) || (delta == 4) || (delta == 0)) return;
// Pull the nearest cog to the magnet.
if (delta > 4) delta -= 8;
else if (delta < -4) delta += 8;
headPos += delta;
if (headPos < 0) headPos = 0; // make a noise?
//else if (headPos > 136) headPos = 136;
else if (headPos > maxHeadPos) headPos = maxHeadPos;
track = (headPos & -4) / 4;
}
void activate()
{
status.setActive(true);
}
void deactivate()
{
status.setActive(false);
magnets[0..4] = false;
}
ubyte read()
{
return imgData.peek(track);
}
void write(ubyte val)
{
if (writeProtected) return;
bool wasDirty = imgData.isDirty;
imgData.poke(track, val);
if (imgData.isDirty && !wasDirty) status.setDirty(true);
}
bool ejectImage()
{
if (imgData.isDirty)
{
int response = TwoappleDialog.run("Warning",
"Save image currently in drive?",
["Save", "Don't save", "Cancel"]);
if (response == 2) return false;
if (response == 0)
{
if (!saveImage(true)) return false;
}
}
imgData = new NonImage();
imgFile = null;
return true;
}
bool loadImage(TwoappleFile file)
{
if (imgData.isDirty)
{
int response = TwoappleDialog.run("Warning",
"Save image currently in drive?",
["Save", "Don't save", "Cancel"]);
if (response == 2) return false;
if (response == 0)
{
if (!saveImage(true)) return false;
}
}
imgFile = ExternalImage.loadImage(file);
assert(imgFile !is null);
imgData = imgFile.imgData;
return true;
}
bool saveImage(bool indirect = false)
{
bool retry, success;
void chooseAction(string msg, void delegate() firstAction = null,
string firstButton = null)
{
bool chooseAgain;
void delegate()[] actions;
string[] buttons;
void delegate() saveAsNib =
{
TwoappleFile file = TwoappleFilePicker.saveAs("Image file",
imgFile.imgFile.folder(),
imgFile.imgFile.baseName() ~ ".nib");
if (file is null) chooseAgain = true;
else
{
imgFile = new NIBImage(file, imgData);
retry = true;
}
};
void delegate() noSave = { success = true; };
void delegate() cancel = {};
actions.length = buttons.length = 2 +
(indirect ? 1 : 0) +
((firstAction !is null) ? 1 : 0);
actions[length - 1] = cancel;
buttons[length - 1] = "Cancel";
if (indirect)
{
actions[length - 2] = noSave;
buttons[length - 2] = "Don't save";
actions[length - 3] = saveAsNib;
buttons[length - 3] = "Save as NIB";
}
else
{
actions[length - 2] = saveAsNib;
buttons[length - 2] = "Save as NIB";
}
if (firstAction !is null)
{
actions[0] = firstAction;
buttons[0] = firstButton;
}
do
{
int action = TwoappleDialog.run("Warning", msg, buttons);
actions[action]();
} while (chooseAgain);
}
do
{
success = retry = false;
try
{
imgFile.save();
imgData.isDirty = false;
success = true;
}
catch (DSKImage.VolumeException e)
{
chooseAction("Disk volume is not 254",
{ e.ignoreVolume(); retry = true; },
"Ignore and save");
}
catch (DSKImage.DSKException e)
{
chooseAction("Image cannot be saved in DSK format");
}
catch (Exception e)
{
TwoappleError.show(e.msg);
}
} while (retry);
return success;
}
}
class InternalImage
{
bool isDirty;
static const NUM_TRACKS = 35;
static const TRACK_LENGTH = 6656; // XXX No DOS 3.2
abstract ubyte peek(uint track);
abstract void poke(uint track, ubyte val);
}
class NonImage : InternalImage
{
ubyte peek(uint track) { return 0; }
void poke(uint track, ubyte val) {}
}
class RealImage : InternalImage
{
ubyte[][] trackData;
uint currentPos;
this()
{
trackData = new ubyte[][NUM_TRACKS];
for (uint t = 0; t < NUM_TRACKS; ++t)
trackData[t] = new ubyte[TRACK_LENGTH];
}
ubyte peek(uint track)
{
currentPos %= TRACK_LENGTH;
return trackData[track][currentPos++];
}
void poke(uint track, ubyte val)
{
currentPos %= TRACK_LENGTH;
trackData[track][currentPos++] = val;
isDirty = true;
}
}
class ExternalImage
{
TwoappleFile imgFile;
RealImage imgData;
static const string invalidMessage = "Invalid image file";
class ReadOnlyException : Exception
{
this() { super("File is read-only"); }
}
static string isValidImage(TwoappleFile checkFile)
{
uint fsize = checkFile.fileSize();
return (isDSKImage(fsize) || isNIBImage(fsize)) ?
null : invalidMessage;
}
static ExternalImage loadImage(TwoappleFile checkFile)
{
uint fsize = checkFile.fileSize();
if (isDSKImage(fsize)) return new DSKImage(checkFile);
if (isNIBImage(fsize)) return new NIBImage(checkFile);
return null;
}
static bool isDSKImage(uint fsize)
{
return (fsize == 143488) ||
((fsize >= 143358) && (fsize <= 143363));
}
static bool isNIBImage(uint fsize)
{
return (fsize == 232960);
}
this(TwoappleFile checkFile)
{
imgFile = checkFile;
imgData = new RealImage();
load();
}
this(TwoappleFile checkFile, InternalImage data)
{
imgFile = checkFile;
imgData = cast(RealImage)data;
}
abstract void load();
abstract void writeOut(File stream);
void save()
{
//if (!imgFile.canWrite()) throw new ReadOnlyException();
File stream = new File(imgFile.fileName, FileMode.Out);
scope(exit)
{
if (stream !is null) stream.close();
}
writeOut(stream);
}
}
class DSKImage : ExternalImage
{
static int[16] dosOrder = [
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F];
static int[16] prodosOrder = [
0x00, 0x08, 0x01, 0x09, 0x02, 0x0A, 0x03, 0x0B,
0x04, 0x0C, 0x05, 0x0D, 0x06, 0x0E, 0x07, 0x0F];
static ubyte[3] DATA_PROLOGUE = [0xD5, 0xAA, 0xAD];
static ubyte[3] ADDR_PROLOGUE = [0xD5, 0xAA, 0x96];
static ubyte[3] EPILOGUE = [0xDE, 0xAA, 0xEB];
static ubyte[0x40] diskByte = [
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[0x70] memByte;
static const NUM_SECTORS = 16;
static const SECTOR_LENGTH = 256;
static const DATA_FIELD_LENGTH = 342;
static this()
{
memByte[0..0x70] = -1;
for (int i = 0; i < 0x40; ++i)
{
memByte[diskByte[i] - 0x96] = i;
}
}
class VolumeException : Exception
{
DSKImage img;
this()
{
super("");
img = this.outer;
}
void ignoreVolume()
{
img.preserveVolume = false;
}
}
class DSKException : Exception
{
this() { super(""); }
}
uint dataOffset;
ubyte[] mbHeader;
bool isProdos;
ubyte volume;
bool preserveVolume;
this(TwoappleFile checkFile)
{
super(checkFile);
volume = 0xFE;
preserveVolume = true;
}
this(TwoappleFile checkFile, InternalImage data)
{
super(checkFile, data);
}
void checkMacBinary(File stream)
{
if (imgFile.fileSize() == 143488)
{
dataOffset = 128;
mbHeader = new ubyte[128];
stream.read(mbHeader);
}
}
bool prodosOrderProdos(File stream)
{
ubyte checkLo, checkHi;
ushort check1, check2, match1, match2;
for (int s = 2; s <= 5; ++s)
{
stream.seekSet(s * 512 + dataOffset);
stream.read(checkLo); stream.read(checkHi);
check1 = (checkHi << 8) | checkLo;
stream.read(checkLo); stream.read(checkHi);
check2 = (checkHi << 8) | checkLo;
match1 = ((s == 2) ? 0 : (s - 1));
match2 = ((s == 5) ? 0 : (s + 1));
if ((check1 != match1) || (check2 != match2)) return false;
}
return true;
}
bool prodosOrderDos(File stream)
{
ubyte check;
for (int s = 5; s <= 13; ++s)
{
stream.seekSet(dataOffset + 0x11002 + (s * 256));
stream.read(check);
if (check != (14 - s)) return false;
}
return true;
}
void loadTrack(File stream, int track)
{
uint offset;
ubyte[] trackData = imgData.trackData[track];
void loadBytes(ubyte[] data)
{
uint len = data.length;
trackData[offset..offset+len] = data;
offset += len;
}
void loadByte(ubyte data, uint len = 1)
{
trackData[offset..offset+len] = data;
offset += len;
}
void loadAddrField(ubyte sector)
{
void encode44(ubyte val)
{
loadByte((((val >> 1) & 0x55) | 0xAA));
loadByte(((val & 0x55) | 0xAA));
}
loadBytes(ADDR_PROLOGUE[0..3]);
encode44(0xFE); // volume
encode44(track);
encode44(sector);
encode44(0xFE ^ track ^ sector); // check byte
loadBytes(EPILOGUE[0..2]);
}
void loadDataField(ubyte sector)
{
loadBytes(DATA_PROLOGUE[0..3]);
loadBytes(loadSector(stream, track, sector));
loadBytes(EPILOGUE[0..3]);
}
loadByte(0xFF, 48); // Load gap 1
for (ubyte sector = 0; sector < NUM_SECTORS; ++sector)
{
loadAddrField(sector);
loadByte(0xFF, 6); // Load gap 2
loadDataField(sector);
loadByte(0xFF, 45); // Load gap 3
}
}
ubyte[] loadSector(File stream, int track, int sector)
{
ubyte[] sectorData = new ubyte[SECTOR_LENGTH];
ubyte[] trackData = new ubyte[DATA_FIELD_LENGTH + 1];
stream.seekSet(dataOffset + (track * (NUM_SECTORS * SECTOR_LENGTH)) +
(isProdos ? (prodosOrder[sector] * SECTOR_LENGTH) :
(dosOrder[sector] * SECTOR_LENGTH)));
stream.read(sectorData);
int x = 0x55;
ubyte y = 2;
uint val;
// Translate 256 bytes of data into 342 6-bit index values
while(true)
{
--y;
val = sectorData[y];
// index values 0 through 85 are composed of a combination
// of the two least significant bits from each data value
// index 85 from data 85, 171, 1
// index 84 from data 84, 170, 0
// index 83 from data 83, 169, 255
// ...
// index 0 from data 0, 86, 172
trackData[x] =
(trackData[x] << 2) |
(((val & 0x01) << 1) | ((val & 0x02) >> 1));
// index values 86 through 341 are composed of the six
// most significant bits of data values 0 through 255
trackData[y + 0x56] = (val >> 2);
--x;
if (x >= 0x00) continue;
x = 0x55;
if (y == 0) break;
}
// Translate the 342 index values into 343 disk bytes
// (where the 343rd disk byte is a check byte)
ubyte lastByte = 0, indexByte;
for (int i = 0; i < DATA_FIELD_LENGTH; ++i)
{
indexByte = trackData[i] & 0x3F;
trackData[i] = diskByte[lastByte ^ indexByte];
lastByte = indexByte;
}
trackData[DATA_FIELD_LENGTH] = diskByte[lastByte];
return trackData;
}
void load()
{
File stream = new File(imgFile.fileName);
checkMacBinary(stream);
isProdos = (prodosOrderDos(stream) || prodosOrderProdos(stream));
stream.seekSet(dataOffset);
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
loadTrack(stream, t);
stream.close();
}
bool writeTrack(int track, ubyte[] saveData)
{
uint offset;
ubyte sector;
int expectedSector = -1;
bool sectorsWritten[] = new bool[NUM_SECTORS];
bool sectorsSeen[] = new bool[NUM_SECTORS];
ubyte sectorData[] = new ubyte[DATA_FIELD_LENGTH];
ubyte[] trackData = imgData.trackData[track];
ubyte nextData(uint delta)
{
return trackData[(offset + delta) % InternalImage.TRACK_LENGTH];
}
bool dataMatches(uint delta, ubyte[] data)
{
for (int b = 0; b < data.length; ++b)
{
if (nextData(delta + b) != data[b])
return false;
}
return true;
}
bool findAddressField()
{
ubyte decode44(ubyte first, ubyte second)
{
return ((first & 0x55) << 1) | (second & 0x55);
}
bool testAddressField()
{
ubyte check, storedVolume, storedTrack;
storedVolume = decode44(nextData(3), nextData(4));
if (storedVolume != 0xFE) volume = storedVolume;
storedTrack = decode44(nextData(5), nextData(6));
sector = decode44(nextData(7), nextData(8));
check = decode44(nextData(9), nextData(10));
if (expectedSector == -1)
expectedSector = sector;
else
expectedSector = (expectedSector + 1) % NUM_SECTORS;
offset += 13;
return (storedTrack == track) && (sector < NUM_SECTORS) &&
(expectedSector == sector) &&
(check == (storedVolume ^ storedTrack ^ sector)) &&
(!sectorsSeen[sector]);
}
while (offset < InternalImage.TRACK_LENGTH)
{
if (dataMatches(0, ADDR_PROLOGUE) &&
dataMatches(11, EPILOGUE[0..2]))
return testAddressField();
else
++offset;
}
return false;
}
bool findDataField()
{
offset %= InternalImage.TRACK_LENGTH;
while (offset < InternalImage.TRACK_LENGTH)
{
if (dataMatches(0, DATA_PROLOGUE) &&
dataMatches(346, EPILOGUE))
{
offset += 3;
sectorsSeen[sector] = true;
return true;
}
else
++offset;
}
return false;
}
void writeDataBlock()
{
uint sectorFirst = (InternalImage.TRACK_LENGTH - offset);
if (sectorFirst > DATA_FIELD_LENGTH)
sectorFirst = DATA_FIELD_LENGTH;
uint sectorSecond = DATA_FIELD_LENGTH - sectorFirst;
sectorData[0..sectorFirst] = trackData[offset..offset+sectorFirst];
if (sectorSecond)
{
sectorData[sectorFirst..sectorFirst+sectorSecond] =
trackData[0..sectorSecond];
}
sectorsWritten[sector] =
writeSector(saveData, sectorData, sector);
}
while (true)
{
if (!findAddressField()) break;
if (!findDataField()) break;
writeDataBlock();
}
for (int sect = 0; sect < NUM_SECTORS; ++sect)
{
if (!sectorsWritten[sect]) return false;
}
return true;
}
bool writeSector(ubyte[] saveData, ubyte[] sectorData, ubyte sector)
{
uint dskOffset = (isProdos ? (prodosOrder[sector] * SECTOR_LENGTH) :
(dosOrder[sector] * SECTOR_LENGTH));
ubyte[] dskData = saveData[dskOffset..dskOffset+SECTOR_LENGTH];
// Translate the 342 disk bytes into 6-bit index values
ubyte[] indexData = new ubyte[DATA_FIELD_LENGTH];
int lastByte = 0;
ubyte nibByte;
for (int i = 0; i < 0x156; ++i)
{
if (sectorData[i] < 0x96) return false;
nibByte = memByte[sectorData[i] - 0x96];
if (nibByte == -1) return false;
indexData[i] = lastByte ^ nibByte;
lastByte = indexData[i];
}
// TODO: verify the checksum
// Translate the 342 index values into 256 bytes
ubyte y = 0;
uint x = 0;
while(true)
{
// The lower two bits of each byte are taken from
// a pair of bits from index values 0 through 85
dskData[y] =
((indexData[x] & 0x01) << 1) | ((indexData[x] & 0x02) >> 1);
indexData[x] >>= 2;
// The upper six bits of bytes 0 through 255 are taken
// from the lower six bits of index values 86 through 342.
dskData[y] |= (indexData[y + 0x56] << 2);
++y;
if (y == 0) break;
++x;
if (x == 0x56) x = 0;
}
return true;
}
void writeOut(File stream)
{
ubyte[][] saveData;
saveData = new ubyte[][InternalImage.NUM_TRACKS];
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
{
saveData[t] = new ubyte[NUM_SECTORS * SECTOR_LENGTH];
if (!(writeTrack(t, saveData[t])))
throw new DSKException();
}
if ((volume != 0xFE) && preserveVolume)
throw new VolumeException();
if (mbHeader.length != 0) stream.write(mbHeader);
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
stream.write(saveData[t]);
}
}
class NIBImage : ExternalImage
{
this(TwoappleFile checkFile) { super(checkFile); }
this(TwoappleFile checkFile, InternalImage data)
{
super(checkFile, data);
}
void load()
{
File stream = new File(imgFile.fileName);
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
{
stream.read(imgData.trackData[t]);
}
stream.close();
}
void writeOut(File stream)
{
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
{
stream.write(imgData.trackData[t]);
}
}
}