mirror of
https://github.com/livingcomputermuseum/ContrAlto.git
synced 2026-02-22 15:18:06 +00:00
615 lines
20 KiB
C#
615 lines
20 KiB
C#
/*
|
|
This file is part of ContrAlto.
|
|
|
|
ContrAlto is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
ContrAlto 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 Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with ContrAlto. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
using System;
|
|
using System.IO;
|
|
|
|
namespace Contralto.IO
|
|
{
|
|
|
|
/// <summary>
|
|
/// DiskGeometry encapsulates the geometry of a disk.
|
|
/// </summary>
|
|
public struct DiskGeometry
|
|
{
|
|
public DiskGeometry(int cylinders, int heads, int sectors, SectorGeometry sectorGeometry)
|
|
{
|
|
Cylinders = cylinders;
|
|
Heads = heads;
|
|
Sectors = sectors;
|
|
SectorGeometry = sectorGeometry;
|
|
}
|
|
|
|
public int Cylinders;
|
|
public int Heads;
|
|
public int Sectors;
|
|
public SectorGeometry SectorGeometry;
|
|
|
|
/// <summary>
|
|
/// Returns the total size (in bytes) of a disk with this geometry.
|
|
/// This includes the extra word-per-sector of the Bitsavers images.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public int GetDiskSizeBytes()
|
|
{
|
|
return SectorGeometry.GetSectorSizeBytes() * (Sectors * Heads * Cylinders);
|
|
}
|
|
|
|
//
|
|
// Standard Diablo geometries
|
|
//
|
|
public static readonly DiskGeometry Diablo31 = new DiskGeometry(203, 2, 12, SectorGeometry.Diablo);
|
|
public static readonly DiskGeometry Diablo44 = new DiskGeometry(406, 2, 12, SectorGeometry.Diablo);
|
|
|
|
//
|
|
// Standard Trident geometries
|
|
//
|
|
public static readonly DiskGeometry TridentT80 = new DiskGeometry(815, 5, 9, SectorGeometry.Trident);
|
|
public static readonly DiskGeometry TridentT300 = new DiskGeometry(815, 19, 9, SectorGeometry.Trident);
|
|
public static readonly DiskGeometry Shugart4004 = new DiskGeometry(202, 4, 8, SectorGeometry.Trident);
|
|
public static readonly DiskGeometry Shugart4008 = new DiskGeometry(202, 8, 8, SectorGeometry.Trident);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Describes the geometry of an Alto disk sector in terms of the
|
|
/// size of the header, label, and data blocks.
|
|
/// </summary>
|
|
public struct SectorGeometry
|
|
{
|
|
/// <summary>
|
|
/// Specifies the layout of a sector on an Alto pack. Sizes are in bytes.
|
|
/// </summary>
|
|
public SectorGeometry(int headerSizeBytes, int labelSizeBytes, int dataSizeBytes)
|
|
{
|
|
HeaderSizeBytes = headerSizeBytes;
|
|
LabelSizeBytes = labelSizeBytes;
|
|
DataSizeBytes = dataSizeBytes;
|
|
|
|
HeaderSize = HeaderSizeBytes / 2;
|
|
LabelSize = LabelSizeBytes / 2;
|
|
DataSize = DataSizeBytes / 2;
|
|
}
|
|
|
|
public int HeaderSize;
|
|
public int LabelSize;
|
|
public int DataSize;
|
|
|
|
public int HeaderSizeBytes;
|
|
public int LabelSizeBytes;
|
|
public int DataSizeBytes;
|
|
|
|
/// <summary>
|
|
/// Returns the total size (in bytes) of a sector with this geometry.
|
|
/// This includes the extra word-per-sector of the Bitsavers images.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public int GetSectorSizeBytes()
|
|
{
|
|
return DataSizeBytes +
|
|
LabelSizeBytes +
|
|
HeaderSizeBytes +
|
|
2; // Extra dummy word
|
|
}
|
|
|
|
public static readonly SectorGeometry Diablo = new SectorGeometry(4, 16, 512);
|
|
public static readonly SectorGeometry Trident = new SectorGeometry(4, 20, 2048);
|
|
}
|
|
|
|
/// <summary>
|
|
/// DiskSector encapsulates the records contained in a single Alto disk sector
|
|
/// on a disk. This includes the header, label, and data records.
|
|
/// </summary>
|
|
public class DiskSector
|
|
{
|
|
/// <summary>
|
|
/// Create a new DiskSector populated from the specified stream.
|
|
/// </summary>
|
|
/// <param name="geometry"></param>
|
|
/// <param name="inputStream"></param>
|
|
/// <param name="cylinder"></param>
|
|
/// <param name="head"></param>
|
|
/// <param name="sector"></param>
|
|
public DiskSector(SectorGeometry geometry, Stream inputStream, int cylinder, int head, int sector)
|
|
{
|
|
//
|
|
// Read in the sector from the input stream.
|
|
//
|
|
byte[] header = new byte[geometry.HeaderSizeBytes];
|
|
byte[] label = new byte[geometry.LabelSizeBytes];
|
|
byte[] data = new byte[geometry.DataSizeBytes];
|
|
|
|
//
|
|
// Bitsavers images have an extra word in the header for some reason.
|
|
// ignore it.
|
|
// TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.)
|
|
//
|
|
inputStream.Seek(2, SeekOrigin.Current);
|
|
|
|
if (inputStream.Read(header, 0, header.Length) != header.Length)
|
|
{
|
|
throw new InvalidOperationException("Short read while reading sector header.");
|
|
}
|
|
|
|
if (inputStream.Read(label, 0, label.Length) != label.Length)
|
|
{
|
|
throw new InvalidOperationException("Short read while reading sector label.");
|
|
}
|
|
|
|
if (inputStream.Read(data, 0, data.Length) != data.Length)
|
|
{
|
|
throw new InvalidOperationException("Short read while reading sector data.");
|
|
}
|
|
|
|
_header = GetUShortArray(header);
|
|
_label = GetUShortArray(label);
|
|
_data = GetUShortArray(data);
|
|
|
|
_cylinder = cylinder;
|
|
_head = head;
|
|
_sector = sector;
|
|
|
|
_modified = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new, empty sector.
|
|
/// </summary>
|
|
/// <param name="geometry"></param>
|
|
/// <param name="cylinder"></param>
|
|
/// <param name="head"></param>
|
|
/// <param name="sector"></param>
|
|
public DiskSector(SectorGeometry geometry, int cylinder, int head, int sector)
|
|
{
|
|
_header = new ushort[geometry.HeaderSize];
|
|
_label = new ushort[geometry.LabelSize];
|
|
_data = new ushort[geometry.DataSize];
|
|
|
|
_cylinder = cylinder;
|
|
_head = head;
|
|
_sector = sector;
|
|
|
|
_modified = true;
|
|
}
|
|
|
|
public int Cylinder
|
|
{
|
|
get { return _cylinder; }
|
|
}
|
|
|
|
public int Head
|
|
{
|
|
get { return _head; }
|
|
}
|
|
|
|
public int Sector
|
|
{
|
|
get { return _sector; }
|
|
}
|
|
|
|
public bool Modified
|
|
{
|
|
get { return _modified; }
|
|
}
|
|
|
|
public ushort ReadHeader(int offset)
|
|
{
|
|
return _header[offset];
|
|
}
|
|
|
|
public void WriteHeader(int offset, ushort value)
|
|
{
|
|
_header[offset] = value;
|
|
_modified = true;
|
|
}
|
|
|
|
public ushort ReadLabel(int offset)
|
|
{
|
|
return _label[offset];
|
|
}
|
|
|
|
public void WriteLabel(int offset, ushort value)
|
|
{
|
|
_label[offset] = value;
|
|
_modified = true;
|
|
}
|
|
|
|
public ushort ReadData(int offset)
|
|
{
|
|
return _data[offset];
|
|
}
|
|
|
|
public void WriteData(int offset, ushort value)
|
|
{
|
|
_data[offset] = value;
|
|
_modified = true;
|
|
}
|
|
|
|
public void WriteToStream(Stream s)
|
|
{
|
|
//
|
|
// Bitsavers images have an extra word in the header for some reason.
|
|
// We will follow this standard when writing out.
|
|
// TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.)
|
|
//
|
|
byte[] dummy = new byte[2];
|
|
s.Write(dummy, 0, 2);
|
|
|
|
WriteWordBuffer(s, _header);
|
|
WriteWordBuffer(s, _label);
|
|
WriteWordBuffer(s, _data);
|
|
|
|
_modified = false;
|
|
}
|
|
|
|
private ushort[] GetUShortArray(byte[] data)
|
|
{
|
|
if ((data.Length % 2) != 0)
|
|
{
|
|
throw new InvalidOperationException("Array length must be even.");
|
|
}
|
|
|
|
ushort[] array = new ushort[data.Length / 2];
|
|
|
|
int offset = 0;
|
|
for(int i=0;i<array.Length;i++)
|
|
{
|
|
array[i] = (ushort)((data[offset]) | (data[offset + 1] << 8));
|
|
offset += 2;
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
private void WriteWordBuffer(Stream imageStream, ushort[] buffer)
|
|
{
|
|
// TODO: this is beyond inefficient
|
|
for (int i = 0; i < buffer.Length; i++)
|
|
{
|
|
imageStream.WriteByte((byte)buffer[i]);
|
|
imageStream.WriteByte((byte)(buffer[i] >> 8));
|
|
}
|
|
}
|
|
|
|
private ushort[] _header;
|
|
private ushort[] _label;
|
|
private ushort[] _data;
|
|
|
|
private int _cylinder;
|
|
private int _head;
|
|
private int _sector;
|
|
|
|
private bool _modified;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The IDiskPack interface defines a generic mechanism for creating, loading, storing,
|
|
/// and accessing the sectors of a disk pack.
|
|
/// </summary>
|
|
public interface IDiskPack : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The geometry of this pack.
|
|
/// </summary>
|
|
DiskGeometry Geometry { get; }
|
|
|
|
/// <summary>
|
|
/// The filename of this pack.
|
|
/// </summary>
|
|
string PackName { get; }
|
|
|
|
/// <summary>
|
|
/// Commits the current in-memory image back to the file it came from.
|
|
/// </summary>
|
|
void Save();
|
|
|
|
/// <summary>
|
|
/// Retrieves the specified sector from storage.
|
|
/// </summary>
|
|
/// <param name="cylinder"></param>
|
|
/// <param name="head"></param>
|
|
/// <param name="sector"></param>
|
|
/// <returns></returns>
|
|
DiskSector GetSector(int cylinder, int head, int sector);
|
|
|
|
/// <summary>
|
|
/// Commits this sector back to storage.
|
|
/// </summary>
|
|
/// <param name="sector"></param>
|
|
void CommitSector(DiskSector sector);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// In-memory implementation of IDiskPack. Useful for small disks (e.g. Diablo).
|
|
/// Changes to the in-memory copy are only committed back to the disk image
|
|
/// when Save is invoked.
|
|
/// </summary>
|
|
public class InMemoryDiskPack : IDiskPack
|
|
{
|
|
/// <summary>
|
|
/// Creates a new, empty disk pack with the specified geometry.
|
|
/// </summary>
|
|
/// <param name="geometry"></param>
|
|
/// <param name="path"></param>
|
|
public static InMemoryDiskPack CreateEmpty(DiskGeometry geometry, string path)
|
|
{
|
|
return new InMemoryDiskPack(geometry, path, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads an existing disk pack image.
|
|
/// </summary>
|
|
/// <param name="geometry"></param>
|
|
/// <param name="path"></param>
|
|
/// <returns></returns>
|
|
public static InMemoryDiskPack Load(DiskGeometry geometry, string path)
|
|
{
|
|
return new InMemoryDiskPack(geometry, path, true);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
//
|
|
// Nothing to do here.
|
|
//
|
|
}
|
|
|
|
private InMemoryDiskPack(DiskGeometry geometry, string path, bool load)
|
|
{
|
|
_packName = path;
|
|
_geometry = geometry;
|
|
_sectors = new DiskSector[_geometry.Cylinders, _geometry.Heads, _geometry.Sectors];
|
|
|
|
if (load)
|
|
{
|
|
//
|
|
// Attempt to load in the specified image file.
|
|
//
|
|
using (FileStream imageStream = new FileStream(_packName, FileMode.Open, FileAccess.Read))
|
|
{
|
|
try
|
|
{
|
|
for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++)
|
|
{
|
|
for (int head = 0; head < _geometry.Heads; head++)
|
|
{
|
|
for (int sector = 0; sector < _geometry.Sectors; sector++)
|
|
{
|
|
_sectors[cylinder, head, sector] = new DiskSector(_geometry.SectorGeometry, imageStream, cylinder, head, sector);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (imageStream.Position != imageStream.Length)
|
|
{
|
|
throw new InvalidOperationException("Extra data at end of image file.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
imageStream.Close();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Just initialize a new, empty disk.
|
|
//
|
|
for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++)
|
|
{
|
|
for (int head = 0; head < _geometry.Heads; head++)
|
|
{
|
|
for (int sector = 0; sector < _geometry.Sectors; sector++)
|
|
{
|
|
_sectors[cylinder, head, sector] = new DiskSector(_geometry.SectorGeometry, cylinder, head, sector);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public DiskGeometry Geometry
|
|
{
|
|
get { return _geometry; }
|
|
}
|
|
|
|
public string PackName
|
|
{
|
|
get { return _packName; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits the current in-memory image back to the file from which it was loaded.
|
|
/// </summary>
|
|
public void Save()
|
|
{
|
|
using (FileStream imageStream = new FileStream(_packName, FileMode.Create, FileAccess.Write))
|
|
{
|
|
try
|
|
{
|
|
for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++)
|
|
{
|
|
for (int head = 0; head < _geometry.Heads; head++)
|
|
{
|
|
for (int sector = 0; sector < _geometry.Sectors; sector++)
|
|
{
|
|
_sectors[cylinder, head, sector].WriteToStream(imageStream);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
imageStream.Close();
|
|
}
|
|
}
|
|
}
|
|
|
|
public DiskSector GetSector(int cylinder, int head, int sector)
|
|
{
|
|
//
|
|
// Return the in-memory sector reference.
|
|
//
|
|
return _sectors[cylinder, head, sector];
|
|
}
|
|
|
|
public void CommitSector(DiskSector sector)
|
|
{
|
|
//
|
|
// Update the in-memory sector reference to point to this (possibly new) sector object.
|
|
//
|
|
if (sector.Modified)
|
|
{
|
|
_sectors[sector.Cylinder, sector.Head, sector.Sector] = sector;
|
|
}
|
|
}
|
|
|
|
private string _packName; // The file from whence the data came
|
|
private DiskSector[,,] _sectors; // All of the sectors on disk
|
|
private DiskGeometry _geometry; // The geometry of this disk.
|
|
}
|
|
|
|
/// <summary>
|
|
/// FileBackedDiskPack provides an implementation of IDiskPack where sectors are read into memory
|
|
/// only when requested, and changes are flushed to disk when use of the sector is complete.
|
|
/// </summary>
|
|
public class FileBackedDiskPack : IDiskPack
|
|
{
|
|
/// <summary>
|
|
/// Creates a new, empty disk pack with the specified geometry.
|
|
/// </summary>
|
|
/// <param name="geometry"></param>
|
|
/// <param name="path"></param>
|
|
public static FileBackedDiskPack CreateEmpty(DiskGeometry geometry, string path)
|
|
{
|
|
return new FileBackedDiskPack(geometry, path, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads an existing image.
|
|
/// </summary>
|
|
/// <param name="geometry"></param>
|
|
/// <param name="path"></param>
|
|
/// <returns></returns>
|
|
public static FileBackedDiskPack Load(DiskGeometry geometry, string path)
|
|
{
|
|
return new FileBackedDiskPack(geometry, path, true);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_diskStream != null)
|
|
{
|
|
_diskStream.Close();
|
|
}
|
|
}
|
|
|
|
private FileBackedDiskPack(DiskGeometry geometry, string path, bool load)
|
|
{
|
|
_packName = path;
|
|
_geometry = geometry;
|
|
|
|
if (load)
|
|
{
|
|
//
|
|
// Attempt to open an existing stream for read/write access.
|
|
//
|
|
_diskStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite);
|
|
|
|
//
|
|
// Quick sanity check that the disk image is the right size.
|
|
//
|
|
if (_diskStream.Length != geometry.GetDiskSizeBytes())
|
|
{
|
|
_diskStream.Close();
|
|
_diskStream = null;
|
|
throw new InvalidOperationException("Image size is invalid.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Attempt to initialize a new stream with read/write access.
|
|
//
|
|
_diskStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
|
|
|
|
//
|
|
// And set the size of the stream.
|
|
//
|
|
_diskStream.SetLength(geometry.GetDiskSizeBytes());
|
|
}
|
|
}
|
|
|
|
public DiskGeometry Geometry
|
|
{
|
|
get { return _geometry; }
|
|
}
|
|
|
|
public string PackName
|
|
{
|
|
get { return _packName; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits pending changes back to disc.
|
|
/// </summary>
|
|
/// <param name="imageStream"></param>
|
|
public void Save()
|
|
{
|
|
// Nothing here, we expect CommitSector
|
|
// to be called by whoever has a sector checked out before shutdown.
|
|
}
|
|
|
|
public DiskSector GetSector(int cylinder, int head, int sector)
|
|
{
|
|
//
|
|
// Retrieve the appropriate sector from disk.
|
|
// Seek to the appropriate position and read.
|
|
//
|
|
_diskStream.Position = GetOffsetForSector(cylinder, head, sector);
|
|
|
|
return new DiskSector(_geometry.SectorGeometry, _diskStream, cylinder, head, sector);
|
|
}
|
|
|
|
public void CommitSector(DiskSector sector)
|
|
{
|
|
if (sector.Modified)
|
|
{
|
|
//
|
|
// Commit this data back to disk.
|
|
// Seek to the appropriate position and flush.
|
|
//
|
|
_diskStream.Position = GetOffsetForSector(sector.Cylinder, sector.Head, sector.Sector);
|
|
sector.WriteToStream(_diskStream);
|
|
}
|
|
}
|
|
|
|
private long GetOffsetForSector(int cylinder, int head, int sector)
|
|
{
|
|
int sectorNumber = (cylinder * _geometry.Heads + head) * _geometry.Sectors + // total sectors for current track
|
|
sector; // + current sector
|
|
|
|
return sectorNumber * _geometry.SectorGeometry.GetSectorSizeBytes();
|
|
}
|
|
|
|
private string _packName; // The file from whence the data came
|
|
private FileStream _diskStream; // The disk image stream containing this disk's contents.
|
|
private DiskGeometry _geometry; // The geometry of this disk.
|
|
}
|
|
}
|