Files

125 lines
5.9 KiB
C#

using System.Buffers.Binary;
using System.Text;
namespace MatrixImageEditor;
public sealed class Fat16Disk : IDisposable
{
private readonly FileStream stream;
private readonly long fatOffset;
private readonly long rootOffset;
private readonly int rootEntries;
private readonly int fatBytes;
private readonly long dataOffset;
private readonly int clusterBytes;
public Fat16Disk(string path, bool writable)
{
stream = new FileStream(path, writable ? FileMode.Open : FileMode.Open, writable ? FileAccess.ReadWrite : FileAccess.Read, FileShare.Read, 1024 * 1024, FileOptions.RandomAccess);
var mbr = ReadAt(0, 512);
if (mbr[510] != 0x55 || mbr[511] != 0xAA) throw new InvalidDataException("文件没有有效的 MBR 分区表");
var partitionLba = BinaryPrimitives.ReadUInt32LittleEndian(mbr.AsSpan(454, 4));
var partitionSectors = BinaryPrimitives.ReadUInt32LittleEndian(mbr.AsSpan(458, 4));
var partitionType = mbr[450];
// WalnutPi labels this BPB-compatible FAT16 volume as 0x0C in the MBR.
if (partitionLba == 0 || partitionSectors == 0 || partitionType is not (0x04 or 0x06 or 0x0B or 0x0C or 0x0E)) throw new InvalidDataException("镜像第一分区不是受支持的 FAT 启动分区");
var partitionOffset = partitionLba * 512L;
if (partitionOffset + partitionSectors * 512L > stream.Length) throw new InvalidDataException("镜像分区表超出文件边界");
var bpb = ReadAt(partitionOffset, 512);
var bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(bpb.AsSpan(11, 2));
var sectorsPerCluster = bpb[13];
var reserved = BinaryPrimitives.ReadUInt16LittleEndian(bpb.AsSpan(14, 2));
var fatCount = bpb[16];
rootEntries = BinaryPrimitives.ReadUInt16LittleEndian(bpb.AsSpan(17, 2));
var fatSectors = BinaryPrimitives.ReadUInt16LittleEndian(bpb.AsSpan(22, 2));
if (bytesPerSector != 512 || sectorsPerCluster == 0 || fatSectors == 0 || rootEntries == 0) throw new InvalidDataException("镜像第一分区不是受支持的 FAT16");
clusterBytes = bytesPerSector * sectorsPerCluster;
fatBytes = fatSectors * bytesPerSector;
fatOffset = partitionOffset + reserved * bytesPerSector;
rootOffset = fatOffset + fatCount * fatBytes;
var rootSectors = (rootEntries * 32L + bytesPerSector - 1) / bytesPerSector;
dataOffset = rootOffset + rootSectors * bytesPerSector;
}
public byte[] ReadFile(string name)
{
var entry = Find(name);
var fat = ReadAt(fatOffset, fatBytes);
var result = new byte[entry.Size];
var written = 0;
foreach (var cluster in Chain(entry.FirstCluster, fat))
{
var count = Math.Min(clusterBytes, result.Length - written);
ReadAt(ClusterOffset(cluster), result.AsSpan(written, count));
written += count;
if (written == result.Length) break;
}
if (written != result.Length) throw new InvalidDataException("镜像配置文件被截断");
return result;
}
public void WriteFileRange(string name, int fileOffset, byte[] data)
{
var entry = Find(name);
if (fileOffset < 0 || fileOffset + data.Length > entry.Size) throw new ArgumentOutOfRangeException(nameof(fileOffset));
var fat = ReadAt(fatOffset, fatBytes);
var chain = Chain(entry.FirstCluster, fat).ToArray();
var remaining = data.Length;
var sourceOffset = 0;
var position = fileOffset;
while (remaining > 0)
{
var chainIndex = position / clusterBytes;
var within = position % clusterBytes;
if (chainIndex >= chain.Length) throw new InvalidDataException("镜像配置文件簇链不足");
var count = Math.Min(remaining, clusterBytes - within);
stream.Position = ClusterOffset(chain[chainIndex]) + within;
stream.Write(data, sourceOffset, count);
position += count; sourceOffset += count; remaining -= count;
}
stream.Flush(flushToDisk: true);
}
private (ushort FirstCluster, int Size) Find(string name)
{
var expected = Name83(name);
var root = ReadAt(rootOffset, rootEntries * 32);
for (var index = 0; index < rootEntries; index++)
{
var entry = root.AsSpan(index * 32, 32);
if (entry[0] == 0x00) break;
if (entry[0] == 0xE5 || entry[11] == 0x0F) continue;
if (entry[..11].SequenceEqual(expected))
{
return (BinaryPrimitives.ReadUInt16LittleEndian(entry.Slice(26, 2)), BinaryPrimitives.ReadInt32LittleEndian(entry.Slice(28, 4)));
}
}
throw new InvalidDataException($"镜像缺少 {name}");
}
private IEnumerable<ushort> Chain(ushort first, byte[] fat)
{
var seen = new HashSet<ushort>();
var cluster = first;
while (cluster is >= 2 and < 0xFFF8)
{
if (!seen.Add(cluster)) throw new InvalidDataException("FAT16 簇链循环");
yield return cluster;
cluster = BinaryPrimitives.ReadUInt16LittleEndian(fat.AsSpan(cluster * 2, 2));
}
}
private long ClusterOffset(ushort cluster) => dataOffset + (cluster - 2L) * clusterBytes;
private byte[] ReadAt(long offset, int count) { var value = new byte[count]; ReadAt(offset, value); return value; }
private void ReadAt(long offset, Span<byte> destination) { stream.Position = offset; stream.ReadExactly(destination); }
private static byte[] Name83(string name)
{
var parts = name.ToUpperInvariant().Split('.', 2);
if (parts[0].Length is < 1 or > 8 || (parts.Length == 2 && parts[1].Length > 3)) throw new ArgumentException("文件名不是 8.3 格式");
return Encoding.ASCII.GetBytes(parts[0].PadRight(8) + (parts.Length == 2 ? parts[1] : "").PadRight(3));
}
public void Dispose() => stream.Dispose();
}