[acorn] Performance optimizations and code parity.

This commit is contained in:
2026-02-08 07:16:25 +00:00
parent 1a9942920e
commit 1b78bb7a85
8 changed files with 566 additions and 161 deletions

View File

@@ -41,26 +41,53 @@ public sealed partial class AcornADFS : IReadOnlyFilesystem
{
const string MODULE_NAME = "ADFS Plugin";
/// <summary>Cached root directory entries (filename -> DirectoryEntryInfo)</summary>
Dictionary<string, DirectoryEntryInfo> _rootDirectoryCache;
/// <summary>Block size in bytes (ADFS sector size)</summary>
int _blockSize;
/// <summary>Cached subdirectory entries (indAddr -> entries dictionary)</summary>
Dictionary<uint, Dictionary<string, DirectoryEntryInfo>> _directoryCache;
/// <summary>Disc record (for new formats)</summary>
DiscRecord _discRecord;
/// <summary>Total disc size in bytes</summary>
ulong _discSize;
/// <summary>Encoding used for filenames</summary>
Encoding _encoding;
/// <summary>IDs per zone</summary>
uint _idsPerZone;
/// <summary>Image plugin being accessed</summary>
IMediaImage _imagePlugin;
/// <summary>Whether filesystem is mounted</summary>
bool _mounted;
/// <summary>Image sector size in bytes</summary>
uint _imageSectorSize;
/// <summary>Partition being mounted</summary>
Partition _partition;
/// <summary>Whether this is a big directory format (F+)</summary>
bool _isBigDirectory;
/// <summary>Whether this is an old map format (ADFS-S, ADFS-M, ADFS-L, ADFS-D)</summary>
bool _isOldMap;
/// <summary>Disc record (for new formats)</summary>
DiscRecord _discRecord;
/// <summary>Log2 of bytes per map bit</summary>
int _log2BytesPerMapBit;
/// <summary>Map bits to block shift (log2bpmb - log2secsize)</summary>
int _map2blk;
/// <summary>Cached zone map data (zone index -> zone data)</summary>
byte[][] _mapCache;
/// <summary>Maximum filename length</summary>
int _maxNameLen;
/// <summary>Whether filesystem is mounted</summary>
bool _mounted;
/// <summary>Number of zones in the map</summary>
int _nzones;
/// <summary>Old map sector 0 (for old formats)</summary>
OldMapSector0 _oldMap0;
@@ -68,26 +95,20 @@ public sealed partial class AcornADFS : IReadOnlyFilesystem
/// <summary>Old map sector 1 (for old formats)</summary>
OldMapSector1 _oldMap1;
/// <summary>Partition being mounted</summary>
Partition _partition;
/// <summary>Root directory indirect disc address</summary>
uint _rootDirectoryAddress;
/// <summary>Cached root directory entries (filename -> DirectoryEntryInfo)</summary>
Dictionary<string, DirectoryEntryInfo> _rootDirectoryCache;
/// <summary>Root directory size in bytes</summary>
uint _rootDirectorySize;
/// <summary>Whether this is a big directory format (F+)</summary>
bool _isBigDirectory;
/// <summary>Block size in bytes</summary>
int _blockSize;
/// <summary>Log2 of bytes per map bit</summary>
int _log2BytesPerMapBit;
/// <summary>Maximum filename length</summary>
int _maxNameLen;
/// <summary>Total disc size in bytes</summary>
ulong _discSize;
/// <summary>Zone size in bits</summary>
int _zoneSize;
/// <inheritdoc />
public FileSystem Metadata { get; private set; }

View File

@@ -32,6 +32,7 @@ using System.Linq;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Filesystems;
@@ -154,7 +155,10 @@ public sealed partial class AcornADFS
/// <returns>Error number indicating success or failure</returns>
ErrorNumber ReadDirectoryContents(uint indAddr, out Dictionary<string, DirectoryEntryInfo> entries)
{
entries = new Dictionary<string, DirectoryEntryInfo>();
// Check cache first
if(_directoryCache.TryGetValue(indAddr, out entries)) return ErrorNumber.NoError;
entries = new Dictionary<string, DirectoryEntryInfo>(StringComparer.OrdinalIgnoreCase);
// Determine directory size based on format
uint dirSize = _isBigDirectory
@@ -182,9 +186,17 @@ public sealed partial class AcornADFS
if(err != ErrorNumber.NoError) return err;
if(_isBigDirectory) return ParseBigDirectoryToDict(dirData, entries);
if(_isBigDirectory)
err = ParseBigDirectoryToDict(dirData, entries);
else
err = ParseStandardDirectoryToDict(dirData, entries);
return ParseStandardDirectoryToDict(dirData, entries);
if(err != ErrorNumber.NoError) return err;
// Cache the result (limit cache size to avoid memory issues)
if(_directoryCache.Count < 1000) _directoryCache[indAddr] = entries;
return ErrorNumber.NoError;
}
/// <summary>Reads directory data from the specified indirect disc address</summary>
@@ -217,11 +229,10 @@ public sealed partial class AcornADFS
else
byteOffset = indAddr * 256UL; // Subdirectory - multiply by sector size
ulong sector = byteOffset / _imagePlugin.Info.SectorSize + _partition.Start;
var offsetInSect = (int)(byteOffset % _imagePlugin.Info.SectorSize);
ulong sector = byteOffset / _imageSectorSize + _partition.Start;
var offsetInSect = (int)(byteOffset % _imageSectorSize);
var sectorsToRead = (uint)((offsetInSect + size + _imagePlugin.Info.SectorSize - 1) /
_imagePlugin.Info.SectorSize);
var sectorsToRead = (uint)((offsetInSect + size + _imageSectorSize - 1) / _imageSectorSize);
if(sector + sectorsToRead > _partition.End) return ErrorNumber.InvalidArgument;
@@ -233,12 +244,21 @@ public sealed partial class AcornADFS
// For old format directories, when physical sector size > 256 bytes,
// the directory data is interleaved - the tail is at the end of the physical sector
// The directory structure is:
// - Header (5 bytes) + Entries (47 * 26 = 1222 bytes) = 1227 bytes at start
// - Tail (53 bytes) at position (sector_size - 54) within the physical sector
if(size == OLD_DIRECTORY_SIZE && sectorData.Length > OLD_DIRECTORY_SIZE)
{
// Old directory: copy first part (1227 bytes), then tail (53 bytes) from end of sector
// This matches the logic in Info.cs
Array.Copy(sectorData, offsetInSect, data, 0, (int)OLD_DIRECTORY_SIZE - 53);
Array.Copy(sectorData, sectorData.Length - 54, data, (int)OLD_DIRECTORY_SIZE - 54, 53);
// Copy the first part (header + entries, 1227 bytes)
const int tailSize = 53;
const int headSize = (int)OLD_DIRECTORY_SIZE - tailSize;
int tailStart = sectorData.Length - tailSize - 1; // -1 for 0-based indexing
// Copy header and entries
Array.Copy(sectorData, offsetInSect, data, 0, headSize);
// Copy tail from end of physical sector
Array.Copy(sectorData, tailStart, data, headSize, tailSize);
}
else if(offsetInSect + size <= sectorData.Length)
Array.Copy(sectorData, offsetInSect, data, 0, size);
@@ -251,6 +271,12 @@ public sealed partial class AcornADFS
// New format: use the map to look up the fragment
// indAddr contains fragment ID in upper bits and offset in lower 8 bits
// We need to read the directory sector by sector using MapBlock
AaruLogging.Debug(MODULE_NAME,
"ReadDirectoryData (new format): indAddr={0}, size={1}, blockSize={2}",
indAddr,
size,
_blockSize);
data = new byte[size];
var bytesRead = 0;
@@ -262,29 +288,42 @@ public sealed partial class AcornADFS
// Map the sector to get physical location
ErrorNumber errno = MapBlock(indAddr, (uint)logicalSector, out ulong physicalSector);
if(errno != ErrorNumber.NoError) return errno;
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME,
"ReadDirectoryData: MapBlock failed for logical sector {0}: {1}",
logicalSector,
errno);
return errno;
}
AaruLogging.Debug(MODULE_NAME,
"ReadDirectoryData: logicalSector={0} -> physicalSector={1}",
logicalSector,
physicalSector);
// Read the sector - physicalSector is the ADFS sector number
// Account for if image sector size differs from ADFS sector size
ulong imageSector;
int offsetInImageSector;
if(_imagePlugin.Info.SectorSize == (uint)_blockSize)
if(_imageSectorSize == (uint)_blockSize)
{
imageSector = physicalSector + _partition.Start;
offsetInImageSector = 0;
}
else if(_imagePlugin.Info.SectorSize > (uint)_blockSize)
else if(_imageSectorSize > (uint)_blockSize)
{
// Image sectors are larger than ADFS sectors
ulong adfsSecPerImgSec = _imagePlugin.Info.SectorSize / (uint)_blockSize;
ulong adfsSecPerImgSec = _imageSectorSize / (uint)_blockSize;
imageSector = physicalSector / adfsSecPerImgSec + _partition.Start;
offsetInImageSector = (int)(physicalSector % adfsSecPerImgSec * (uint)_blockSize);
}
else
{
// Image sectors are smaller than ADFS sectors
ulong imgSecPerAdfsSec = (uint)_blockSize / _imagePlugin.Info.SectorSize;
ulong imgSecPerAdfsSec = (uint)_blockSize / _imageSectorSize;
imageSector = physicalSector * imgSecPerAdfsSec + _partition.Start;
offsetInImageSector = 0;
}
@@ -396,23 +435,49 @@ public sealed partial class AcornADFS
/// <returns>Error number indicating success or failure</returns>
ErrorNumber ParseBigDirectoryToDict(byte[] dirData, Dictionary<string, DirectoryEntryInfo> entries)
{
if(dirData.Length < 28) // Minimum header size
return ErrorNumber.InvalidArgument;
// Minimum size: header (28) + tail (8)
if(dirData.Length < 36) return ErrorNumber.InvalidArgument;
// Parse big directory header
BigDirectoryHeader header = Marshal.ByteArrayToStructureLittleEndian<BigDirectoryHeader>(dirData);
// Validate header magic
if(header.bigDirStartName != BIG_DIR_START_NAME) return ErrorNumber.InvalidArgument;
uint numEntries = header.bigDirEntries;
uint nameHeapOff = 28 + header.bigDirNameLen; // After header + directory name
uint entriesOff = nameHeapOff;
// Validate version (must be 0.0.0 according to Linux kernel)
if(header.bigDirVersion[0] != 0 || header.bigDirVersion[1] != 0 || header.bigDirVersion[2] != 0)
return ErrorNumber.InvalidArgument;
// Align to 4 bytes
if(entriesOff % 4 != 0) entriesOff += 4 - entriesOff % 4;
// Validate directory size (must be multiple of 2048 and not exceed 4MB)
uint dirSize = header.bigDirSize;
if(dirSize == 0 || (dirSize & 2047) != 0 || dirSize > 4 * 1024 * 1024) return ErrorNumber.InvalidArgument;
// Validate tail magic
if(dirData.Length >= dirSize)
{
BigDirectoryTail tail =
Marshal.ByteArrayToStructureLittleEndian<BigDirectoryTail>(dirData, (int)(dirSize - 8), 8);
if(tail.bigDirEndName != BIG_DIR_END_NAME) return ErrorNumber.InvalidArgument;
// Validate master sequence numbers match
if(tail.bigDirEndMasSeq != header.startMasSeq) return ErrorNumber.InvalidArgument;
}
uint numEntries = header.bigDirEntries;
// Calculate entries offset: header (28 bytes) + directory name (aligned to 4 bytes)
uint entriesOff = 28 + (header.bigDirNameLen + 3 & ~3u);
const int bigEntrySize = 28;
// Validate entries don't overflow
if(numEntries > 4 * 1024 * 1024 / bigEntrySize) return ErrorNumber.InvalidArgument;
// Calculate name heap start (after all entries)
uint nameHeapStart = entriesOff + numEntries * bigEntrySize;
for(uint i = 0; i < numEntries; i++)
{
uint offset = entriesOff + i * bigEntrySize;
@@ -444,7 +509,7 @@ public sealed partial class AcornADFS
ExecAddr = entry.bigDirExec,
Length = entry.bigDirLen,
IndAddr = entry.bigDirIndAddr,
Attributes = (byte)entry.bigDirAttr
Attributes = entry.bigDirAttr
};
// Don't add duplicate entries

View File

@@ -35,6 +35,7 @@ using System.Collections.Generic;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Logging;
namespace Aaru.Filesystems;
@@ -331,6 +332,13 @@ public sealed partial class AcornADFS
uint fragId = indAddr >> 8;
uint fragOffset = indAddr & 0xFF;
AaruLogging.Debug(MODULE_NAME,
"MapBlock: indAddr={0}, logicalBlock={1}, fragId={2}, fragOffset={3}",
indAddr,
logicalBlock,
fragId,
fragOffset);
// Calculate sector offset within the fragment
uint sectorOffset = logicalBlock;
@@ -340,6 +348,11 @@ public sealed partial class AcornADFS
{
int shareSize = _discRecord.flags & 0x0F; // log2sharesize is in lower 4 bits of flags
sectorOffset += fragOffset - 1 << shareSize;
AaruLogging.Debug(MODULE_NAME,
"MapBlock: shareSize={0}, adjusted sectorOffset={1}",
shareSize,
sectorOffset);
}
// Look up the fragment in the map
@@ -355,54 +368,85 @@ public sealed partial class AcornADFS
{
physicalBlock = 0;
int nzones = _discRecord.nzones + (_discRecord.nzones_high << 8);
if(_nzones <= 0)
{
AaruLogging.Debug(MODULE_NAME, "MapLookup: _nzones is {0}, invalid", _nzones);
return ErrorNumber.InvalidArgument;
}
int idlen = _discRecord.idlen;
int sectorSize = 1 << _discRecord.log2secsize;
int zoneSpare = _discRecord.zone_spare;
int log2bpmb = _discRecord.log2bpmb;
int log2secsize = _discRecord.log2secsize;
// s_map2blk is the shift to convert between map bits and sectors
int map2blk = log2bpmb - log2secsize;
// Zone size in bits (excluding zone header and spare)
int zoneSize = sectorSize * 8 - zoneSpare;
// Calculate map address - the map starts near the middle of the disc
// map_addr = (nzones >> 1) * zone_size - ((nzones > 1) ? DISC_RECORD_SIZE * 8 : 0)
// Then convert from bits to sectors using map2blk
int mapAddrBits = (nzones >> 1) * zoneSize - (nzones > 1 ? DISC_RECORD_SIZE * 8 : 0);
int mapAddr = map2blk >= 0 ? mapAddrBits << map2blk : mapAddrBits >> -map2blk;
AaruLogging.Debug(MODULE_NAME,
"MapLookup: fragId={0}, sectorOffset={1}, nzones={2}, idlen={3}, map2blk={4}",
fragId,
sectorOffset,
_nzones,
idlen,
map2blk);
// Calculate which zone to start searching
// Root fragment (ID 2) starts in the middle zone
int idsPerZone = zoneSize / (idlen + 1);
int startZone = fragId == 2 ? nzones / 2 : (int)(fragId / (uint)idsPerZone);
int startZone = fragId == 2 ? _nzones / 2 : (int)(fragId / _idsPerZone);
if(startZone >= nzones) startZone = 0;
if(startZone >= _nzones) startZone = 0;
// Convert sector offset to map offset
uint mapOff = map2blk >= 0 ? sectorOffset >> map2blk : sectorOffset << -map2blk;
AaruLogging.Debug(MODULE_NAME,
"MapLookup: startZone={0}, mapOff={1}, idsPerZone={2}",
startZone,
mapOff,
_idsPerZone);
// Search through zones starting from startZone
for(var zoneCount = 0; zoneCount < nzones; zoneCount++)
for(var zoneCount = 0; zoneCount < _nzones; zoneCount++)
{
int zone = (startZone + zoneCount) % nzones;
int zone = (startZone + zoneCount) % _nzones;
// Calculate the sector address for this zone
ulong zoneSector = (ulong)(mapAddr + zone) + _partition.Start;
// Use cached zone data if available
byte[] zoneData;
// Read the zone
ErrorNumber errno = _imagePlugin.ReadSector(zoneSector, false, out byte[] zoneData, out _);
if(_mapCache != null && _mapCache[zone] != null)
zoneData = _mapCache[zone];
else
{
AaruLogging.Debug(MODULE_NAME, "MapLookup: Zone {0} not cached, reading from disk", zone);
if(errno != ErrorNumber.NoError) continue;
// Fall back to reading from disk if not cached
// Calculate the sector address for this zone following Linux kernel algorithm
long mapAddrBits = (long)(_nzones >> 1) * _zoneSize - (_nzones > 1 ? DISC_RECORD_SIZE * 8 : 0);
long mapAddrSectors;
if(map2blk >= 0)
mapAddrSectors = mapAddrBits << map2blk;
else
mapAddrSectors = mapAddrBits >> -map2blk;
var zoneSector = (ulong)(mapAddrSectors + zone);
ErrorNumber errno = ReadAdfsSector(zoneSector, out zoneData);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "MapLookup: Failed to read zone {0}: {1}", zone, errno);
continue;
}
}
// Calculate zone layout:
// Zone 0: startBit = 32 + DISC_RECORD_SIZE * 8, startBlk = 0
// Other zones: startBit = 32, startBlk = zone * zoneSize - DISC_RECORD_SIZE * 8
int startBit = zone == 0 ? 32 + DISC_RECORD_SIZE * 8 : 32;
int endBit = 32 + zoneSize;
int startBlk = zone == 0 ? 0 : zone * zoneSize - DISC_RECORD_SIZE * 8;
int endBit = 32 + _zoneSize;
int startBlk = zone == 0 ? 0 : zone * _zoneSize - DISC_RECORD_SIZE * 8;
// Search for the fragment in this zone
int result = LookupZone(zoneData, idlen, fragId, ref mapOff, startBit, endBit);
@@ -415,10 +459,18 @@ public sealed partial class AcornADFS
physicalBlock = secOff + (map2blk >= 0 ? (uint)mapResult << map2blk : (uint)mapResult >> -map2blk);
AaruLogging.Debug(MODULE_NAME,
"MapLookup: Found! zone={0}, result={1}, physicalBlock={2}",
zone,
result,
physicalBlock);
return ErrorNumber.NoError;
}
}
AaruLogging.Debug(MODULE_NAME, "MapLookup: Fragment {0} not found in any zone", fragId);
return ErrorNumber.InvalidArgument;
}
@@ -434,11 +486,45 @@ public sealed partial class AcornADFS
{
uint idmask = (1u << idlen) - 1;
// Get the free link at offset 8 (limited to 15 bits)
uint freeLink = (uint)GetBits(zoneData, 8, Math.Min(idlen, 15)) & 0x7FFF;
// Get the free link at offset 8 (limited to 15 bits per spec)
uint freeLink = (uint)GetBits(zoneData, 8, idlen) & 0x7FFF;
uint freelinkPos = freeLink != 0 ? 8 + freeLink : 0;
int position = startBit;
AaruLogging.Debug(MODULE_NAME,
"LookupZone: idlen={0}, fragId={1}, offset={2}, startBit={3}, endBit={4}, freeLink={5}, freelinkPos={6}",
idlen,
fragId,
offset,
startBit,
endBit,
freeLink,
freelinkPos);
// Debug: dump first 16 bytes of zone
if(zoneData.Length >= 16)
{
AaruLogging.Debug(MODULE_NAME,
"LookupZone: zone first 16 bytes: {0:X2} {1:X2} {2:X2} {3:X2} {4:X2} {5:X2} {6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} {12:X2} {13:X2} {14:X2} {15:X2}",
zoneData[0],
zoneData[1],
zoneData[2],
zoneData[3],
zoneData[4],
zoneData[5],
zoneData[6],
zoneData[7],
zoneData[8],
zoneData[9],
zoneData[10],
zoneData[11],
zoneData[12],
zoneData[13],
zoneData[14],
zoneData[15]);
}
int position = startBit;
var fragCount = 0;
while(position < endBit)
{
@@ -448,10 +534,28 @@ public sealed partial class AcornADFS
// Find the end of this fragment (next '1' bit after the fragment ID)
int fragEnd = FindNextSetBit(zoneData, position + idlen, endBit);
if(fragEnd < 0 || fragEnd >= endBit) break;
if(fragEnd < 0 || fragEnd >= endBit)
{
AaruLogging.Debug(MODULE_NAME, "LookupZone: No end bit found at position {0}, breaking", position);
break;
}
int fragLength = fragEnd + 1 - position;
// Log first few fragments for debugging
if(fragCount < 5)
{
AaruLogging.Debug(MODULE_NAME,
"LookupZone: pos={0}, frag={1}, fragEnd={2}, fragLen={3}",
position,
frag,
fragEnd,
fragLength);
}
fragCount++;
// Check if we're at the free link position (skip free space entries)
if(position == freelinkPos)
{
@@ -461,6 +565,13 @@ public sealed partial class AcornADFS
else if(frag == fragId)
{
// Found our fragment
AaruLogging.Debug(MODULE_NAME,
"LookupZone: Found fragId={0} at pos={1}, length={2}, offset={3}",
fragId,
position,
fragLength,
offset);
if(offset < fragLength)
{
// The offset is within this fragment
@@ -474,6 +585,8 @@ public sealed partial class AcornADFS
position = fragEnd + 1;
}
AaruLogging.Debug(MODULE_NAME, "LookupZone: Scanned {0} fragments, not found", fragCount);
return -1; // Not found in this zone
}
@@ -563,8 +676,8 @@ public sealed partial class AcornADFS
Links = 1
};
// Set attributes
var attrs = (FileAttributes)entry.Attributes;
// Set attributes (only low 8 bits are used for standard RISC OS attributes)
var attrs = (FileAttributes)(byte)entry.Attributes;
info.Attributes = attrs.HasFlag(FileAttributes.Directory)
? CommonTypes.Structs.FileAttributes.Directory

View File

@@ -52,8 +52,8 @@ public sealed partial class AcornADFS
/// <summary>Indirect disc address</summary>
public uint IndAddr { get; set; }
/// <summary>File attributes</summary>
public byte Attributes { get; set; }
/// <summary>File attributes (8-bit for standard directories, 32-bit for big directories)</summary>
public uint Attributes { get; set; }
}
/// <summary>Directory node for enumerating directory contents</summary>
@@ -75,18 +75,17 @@ public sealed partial class AcornADFS
/// <summary>File node for reading file contents</summary>
sealed class AcornFileNode : IFileNode
{
/// <summary>Indirect disc address of the file</summary>
internal uint IndAddr { get; init; }
/// <summary>File attributes (8-bit for standard directories, 32-bit for big directories)</summary>
internal uint Attributes { get; init; }
/// <summary>Current read position within the file</summary>
public long Offset { get; set; }
/// <summary>File length in bytes</summary>
public long Length { get; init; }
/// <summary>Indirect disc address of the file</summary>
internal uint IndAddr { get; init; }
/// <summary>File attributes</summary>
internal byte Attributes { get; init; }
/// <inheritdoc />
public string Path { get; init; }
}

View File

@@ -41,21 +41,23 @@ namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class AcornADFS
{
/// <inheritdoc />
public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
Dictionary<string, string> options, string @namespace)
{
AaruLogging.Debug(MODULE_NAME, "Mounting ADFS volume");
_imagePlugin = imagePlugin;
_partition = partition;
_encoding = encoding ?? Encoding.GetEncoding("iso-8859-1");
_rootDirectoryCache = new Dictionary<string, DirectoryEntryInfo>();
_imagePlugin = imagePlugin;
_partition = partition;
_encoding = encoding ?? Encoding.GetEncoding("iso-8859-1");
_imageSectorSize = imagePlugin.Info.SectorSize;
_rootDirectoryCache = new Dictionary<string, DirectoryEntryInfo>(StringComparer.OrdinalIgnoreCase);
_directoryCache = new Dictionary<uint, Dictionary<string, DirectoryEntryInfo>>();
_mapCache = null;
if(imagePlugin.Info.SectorSize < 256)
if(_imageSectorSize < 256)
{
AaruLogging.Debug(MODULE_NAME, "Sector size too small: {0}", imagePlugin.Info.SectorSize);
AaruLogging.Debug(MODULE_NAME, "Sector size too small: {0}", _imageSectorSize);
return ErrorNumber.InvalidArgument;
}
@@ -119,11 +121,14 @@ public sealed partial class AcornADFS
AaruLogging.Debug(MODULE_NAME, "Unmounting volume");
_rootDirectoryCache?.Clear();
_mounted = false;
_imagePlugin = null;
_partition = default;
_encoding = null;
Metadata = null;
_directoryCache?.Clear();
_mapCache = null;
_mounted = false;
_imagePlugin = null;
_partition = default(Partition);
_encoding = null;
_imageSectorSize = 0;
Metadata = null;
AaruLogging.Debug(MODULE_NAME, "Volume unmounted");
@@ -146,22 +151,24 @@ public sealed partial class AcornADFS
ErrorNumber errno = _imagePlugin.ReadSector(0, false, out byte[] sector0, out _);
if(errno != ErrorNumber.NoError)
return errno;
if(errno != ErrorNumber.NoError) return errno;
byte oldChk0 = AcornMapChecksum(sector0, 255);
OldMapSector0 oldMap0 = Marshal.ByteArrayToStructureLittleEndian<OldMapSector0>(sector0);
errno = _imagePlugin.ReadSector(1, false, out byte[] sector1, out _);
if(errno != ErrorNumber.NoError)
return errno;
if(errno != ErrorNumber.NoError) return errno;
byte oldChk1 = AcornMapChecksum(sector1, 255);
OldMapSector1 oldMap1 = Marshal.ByteArrayToStructureLittleEndian<OldMapSector1>(sector1);
AaruLogging.Debug(MODULE_NAME, "TryOldMapFormat: oldMap0.checksum={0}, oldChk0={1}, oldMap1.checksum={2}, oldChk1={3}",
oldMap0.checksum, oldChk0, oldMap1.checksum, oldChk1);
AaruLogging.Debug(MODULE_NAME,
"TryOldMapFormat: oldMap0.checksum={0}, oldChk0={1}, oldMap1.checksum={2}, oldChk1={3}",
oldMap0.checksum,
oldChk0,
oldMap1.checksum,
oldChk1);
// According to documentation map1 MUST start on sector 1.
// On ADFS-D it starts at 0x100, not on sector 1 (0x400)
@@ -173,8 +180,7 @@ public sealed partial class AcornADFS
oldMap1 = Marshal.ByteArrayToStructureLittleEndian<OldMapSector1>(tmp);
}
if(oldMap0.checksum != oldChk0 || oldMap1.checksum != oldChk1 ||
oldMap0.checksum == 0 || oldMap1.checksum == 0)
if(oldMap0.checksum != oldChk0 || oldMap1.checksum != oldChk1 || oldMap0.checksum == 0 || oldMap1.checksum == 0)
return ErrorNumber.InvalidArgument;
_oldMap0 = oldMap0;
@@ -210,8 +216,7 @@ public sealed partial class AcornADFS
}
// If validation failed, reset _isOldMap so TryNewMapFormat works correctly
if(errno != ErrorNumber.NoError)
_isOldMap = false;
if(errno != ErrorNumber.NoError) _isOldMap = false;
return errno;
}
@@ -250,11 +255,13 @@ public sealed partial class AcornADFS
return ErrorNumber.InvalidArgument;
}
errno = _imagePlugin.ReadSectors(sbSector + _partition.Start, false, sectorsToRead, out byte[] bootSector,
errno = _imagePlugin.ReadSectors(sbSector + _partition.Start,
false,
sectorsToRead,
out byte[] bootSector,
out _);
if(errno != ErrorNumber.NoError)
return errno;
if(errno != ErrorNumber.NoError) return errno;
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: bootSector.Length={0}", bootSector.Length);
@@ -267,10 +274,12 @@ public sealed partial class AcornADFS
return ErrorNumber.InvalidArgument;
}
for(var i = 0; i < 0x1FF; i++)
bootChk = (bootChk & 0xFF) + (bootChk >> 8) + bootSector[i];
for(var i = 0; i < 0x1FF; i++) bootChk = (bootChk & 0xFF) + (bootChk >> 8) + bootSector[i];
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: bootChk={0}, bootSector[0x1FF]={1}", bootChk, bootSector[0x1FF]);
AaruLogging.Debug(MODULE_NAME,
"TryNewMapFormat: bootChk={0}, bootSector[0x1FF]={1}",
bootChk,
bootSector[0x1FF]);
DiscRecord drSb;
@@ -281,6 +290,7 @@ public sealed partial class AcornADFS
NewMap nmap = Marshal.ByteArrayToStructureLittleEndian<NewMap>(sector);
drSb = nmap.discRecord;
}
// Check if boot block checksum is valid
else if((bootChk & 0xFF) == bootSector[0x1FF])
{
@@ -295,10 +305,15 @@ public sealed partial class AcornADFS
return ErrorNumber.InvalidArgument;
}
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: log2secsize={0}, idlen={1}, disc_size_high={2}, disc_size={3}",
drSb.log2secsize, drSb.idlen, drSb.disc_size_high, drSb.disc_size);
AaruLogging.Debug(MODULE_NAME,
"TryNewMapFormat: log2secsize={0}, idlen={1}, disc_size_high={2}, disc_size={3}",
drSb.log2secsize,
drSb.idlen,
drSb.disc_size_high,
drSb.disc_size);
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: reserved is null or empty = {0}",
AaruLogging.Debug(MODULE_NAME,
"TryNewMapFormat: reserved is null or empty = {0}",
ArrayHelpers.ArrayIsNullOrEmpty(drSb.reserved));
// Validate disc record
@@ -311,22 +326,29 @@ public sealed partial class AcornADFS
if(drSb.idlen < drSb.log2secsize + 3 || drSb.idlen > 19)
{
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: Invalid idlen ({0} < {1} or > 19)", drSb.idlen, drSb.log2secsize + 3);
AaruLogging.Debug(MODULE_NAME,
"TryNewMapFormat: Invalid idlen ({0} < {1} or > 19)",
drSb.idlen,
drSb.log2secsize + 3);
return ErrorNumber.InvalidArgument;
}
if(drSb.disc_size_high >> drSb.log2secsize != 0)
{
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: disc_size_high validation failed ({0} >> {1} = {2})",
drSb.disc_size_high, drSb.log2secsize, drSb.disc_size_high >> drSb.log2secsize);
AaruLogging.Debug(MODULE_NAME,
"TryNewMapFormat: disc_size_high validation failed ({0} >> {1} = {2})",
drSb.disc_size_high,
drSb.log2secsize,
drSb.disc_size_high >> drSb.log2secsize);
return ErrorNumber.InvalidArgument;
}
if(!ArrayHelpers.ArrayIsNullOrEmpty(drSb.reserved))
{
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: Reserved field not empty (length={0})",
AaruLogging.Debug(MODULE_NAME,
"TryNewMapFormat: Reserved field not empty (length={0})",
drSb.reserved?.Length ?? -1);
return ErrorNumber.InvalidArgument;
@@ -339,34 +361,51 @@ public sealed partial class AcornADFS
_discSize *= 0x100000000;
_discSize += drSb.disc_size;
if(_discSize > _imagePlugin.Info.Sectors * _imagePlugin.Info.SectorSize)
return ErrorNumber.InvalidArgument;
if(_discSize > _imagePlugin.Info.Sectors * _imagePlugin.Info.SectorSize) return ErrorNumber.InvalidArgument;
// Set block size and other parameters
_blockSize = 1 << drSb.log2secsize;
_log2BytesPerMapBit = drSb.log2bpmb;
// Calculate map parameters (following Linux kernel adfs_read_map)
_nzones = drSb.nzones | drSb.nzones_high << 8;
_zoneSize = (8 << drSb.log2secsize) - drSb.zone_spare;
_map2blk = drSb.log2bpmb - drSb.log2secsize;
// IDs per zone: zone_size / (idlen + 1)
_idsPerZone = (uint)(_zoneSize / (drSb.idlen + 1));
// Load and cache the zone map for better performance
ErrorNumber mapErr = LoadZoneMap();
if(mapErr != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "TryNewMapFormat: Failed to load zone map: {0}", mapErr);
return mapErr;
}
// Determine directory format and name length
if(drSb.format_version > 0)
{
// ADFS-G or later: big directories
_isBigDirectory = true;
_maxNameLen = FPLUS_NAME_LEN;
_rootDirectorySize = drSb.root_size;
_isBigDirectory = true;
_maxNameLen = FPLUS_NAME_LEN;
_rootDirectorySize = drSb.root_size;
}
else if((drSb.flags & 0x01) != 0)
{
// ADFS-F+: big directories for discs > 512MB
_isBigDirectory = true;
_maxNameLen = FPLUS_NAME_LEN;
_rootDirectorySize = drSb.root_size;
_isBigDirectory = true;
_maxNameLen = FPLUS_NAME_LEN;
_rootDirectorySize = drSb.root_size;
}
else
{
// ADFS-E or ADFS-F: standard directories
_isBigDirectory = false;
_maxNameLen = F_NAME_LEN;
_rootDirectorySize = NEW_DIRECTORY_SIZE;
_isBigDirectory = false;
_maxNameLen = F_NAME_LEN;
_rootDirectorySize = NEW_DIRECTORY_SIZE;
}
// Root directory address comes from disc record
@@ -380,8 +419,11 @@ public sealed partial class AcornADFS
/// <returns>Error number indicating success or failure</returns>
ErrorNumber ValidateRootDirectory()
{
AaruLogging.Debug(MODULE_NAME, "ValidateRootDirectory: rootAddr={0}, rootSize={1}, isBigDir={2}",
_rootDirectoryAddress, _rootDirectorySize, _isBigDirectory);
AaruLogging.Debug(MODULE_NAME,
"ValidateRootDirectory: rootAddr={0}, rootSize={1}, isBigDir={2}",
_rootDirectoryAddress,
_rootDirectorySize,
_isBigDirectory);
ErrorNumber errno = ReadDirectoryData(_rootDirectoryAddress, _rootDirectorySize, out byte[] dirData);
@@ -404,24 +446,27 @@ public sealed partial class AcornADFS
return ErrorNumber.InvalidArgument;
}
uint startMagic = BitConverter.ToUInt32(dirData, 4);
var startMagic = BitConverter.ToUInt32(dirData, 4);
AaruLogging.Debug(MODULE_NAME, "ValidateRootDirectory: big dir startMagic=0x{0:X8}, expected=0x{1:X8}",
startMagic, BIG_DIR_START_NAME);
AaruLogging.Debug(MODULE_NAME,
"ValidateRootDirectory: big dir startMagic=0x{0:X8}, expected=0x{1:X8}",
startMagic,
BIG_DIR_START_NAME);
if(startMagic != BIG_DIR_START_NAME)
return ErrorNumber.InvalidArgument;
if(startMagic != BIG_DIR_START_NAME) return ErrorNumber.InvalidArgument;
}
else
{
// Check standard directory magic (Hugo or Nick)
DirectoryHeader header = Marshal.ByteArrayToStructureLittleEndian<DirectoryHeader>(dirData);
AaruLogging.Debug(MODULE_NAME, "ValidateRootDirectory: magic=0x{0:X8}, OLD=0x{1:X8}, NEW=0x{2:X8}",
header.magic, OLD_DIR_MAGIC, NEW_DIR_MAGIC);
AaruLogging.Debug(MODULE_NAME,
"ValidateRootDirectory: magic=0x{0:X8}, OLD=0x{1:X8}, NEW=0x{2:X8}",
header.magic,
OLD_DIR_MAGIC,
NEW_DIR_MAGIC);
if(header.magic != OLD_DIR_MAGIC && header.magic != NEW_DIR_MAGIC)
return ErrorNumber.InvalidArgument;
if(header.magic != OLD_DIR_MAGIC && header.magic != NEW_DIR_MAGIC) return ErrorNumber.InvalidArgument;
}
return ErrorNumber.NoError;
@@ -433,12 +478,124 @@ public sealed partial class AcornADFS
{
ErrorNumber errno = ReadDirectoryData(_rootDirectoryAddress, _rootDirectorySize, out byte[] dirData);
if(errno != ErrorNumber.NoError)
return errno;
if(errno != ErrorNumber.NoError) return errno;
if(_isBigDirectory)
return ParseBigDirectory(dirData);
if(_isBigDirectory) return ParseBigDirectory(dirData);
return ParseStandardDirectory(dirData);
}
}
/// <summary>Loads and caches the zone map for new format ADFS volumes</summary>
/// <returns>Error number indicating success or failure</returns>
ErrorNumber LoadZoneMap()
{
if(_nzones <= 0 || _blockSize <= 0) return ErrorNumber.InvalidArgument;
_mapCache = new byte[_nzones][];
// Calculate map start address following Linux kernel algorithm:
// zone_size is in bits: (8 << log2secsize) - zone_spare
// map_addr_bits = (nzones >> 1) * zone_size - ((nzones > 1) ? ADFS_DR_SIZE_BITS : 0)
// map_addr_sectors = signed_asl(map_addr_bits, map2blk)
// where map2blk = log2bpmb - log2secsize
long mapAddrBits = (long)(_nzones >> 1) * _zoneSize - (_nzones > 1 ? DISC_RECORD_SIZE * 8 : 0);
AaruLogging.Debug(MODULE_NAME,
"LoadZoneMap: nzones={0}, zoneSize={1} bits, map2blk={2}, mapAddrBits={3}",
_nzones,
_zoneSize,
_map2blk,
mapAddrBits);
// Apply map2blk shift to convert from bits to sectors
long mapAddrSectors;
if(_map2blk >= 0)
mapAddrSectors = mapAddrBits << _map2blk;
else
mapAddrSectors = mapAddrBits >> -_map2blk;
AaruLogging.Debug(MODULE_NAME, "LoadZoneMap: mapAddrSectors={0}", mapAddrSectors);
// Read each zone's map data - each zone is one sector at map_addr + zone
for(var zone = 0; zone < _nzones; zone++)
{
// Zone sector = map_addr + zone (each zone is one ADFS sector)
var zoneSector = (ulong)(mapAddrSectors + zone);
AaruLogging.Debug(MODULE_NAME, "LoadZoneMap: Reading zone {0} from ADFS sector {1}", zone, zoneSector);
// Handle sector size differences between image and ADFS
ErrorNumber errno = ReadAdfsSector(zoneSector, out byte[] zoneData);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME,
"LoadZoneMap: Failed to read zone {0} at sector {1}: {2}",
zone,
zoneSector,
errno);
return errno;
}
AaruLogging.Debug(MODULE_NAME,
"LoadZoneMap: Zone {0} read, length={1}, first bytes: {2:X2} {3:X2} {4:X2} {5:X2}",
zone,
zoneData.Length,
zoneData.Length > 0 ? zoneData[0] : 0,
zoneData.Length > 1 ? zoneData[1] : 0,
zoneData.Length > 2 ? zoneData[2] : 0,
zoneData.Length > 3 ? zoneData[3] : 0);
_mapCache[zone] = zoneData;
}
return ErrorNumber.NoError;
}
/// <summary>Reads a single ADFS sector, handling image sector size differences</summary>
/// <param name="adfsSector">ADFS sector number</param>
/// <param name="data">Output sector data</param>
/// <returns>Error number indicating success or failure</returns>
ErrorNumber ReadAdfsSector(ulong adfsSector, out byte[] data)
{
data = null;
// Handle sector size differences
if(_imageSectorSize == (uint)_blockSize)
{
// Sector sizes match - direct read
return _imagePlugin.ReadSector(adfsSector + _partition.Start, false, out data, out _);
}
if(_imageSectorSize > (uint)_blockSize)
{
// Image sectors larger than ADFS sectors - read containing sector and extract
ulong adfsSecPerImgSec = _imageSectorSize / (uint)_blockSize;
ulong imageSector = adfsSector / adfsSecPerImgSec + _partition.Start;
var offsetInSector = (int)(adfsSector % adfsSecPerImgSec * (uint)_blockSize);
ErrorNumber errno = _imagePlugin.ReadSector(imageSector, false, out byte[] sectorData, out _);
if(errno != ErrorNumber.NoError) return errno;
data = new byte[_blockSize];
if(offsetInSector + _blockSize <= sectorData.Length)
Array.Copy(sectorData, offsetInSector, data, 0, _blockSize);
else
Array.Copy(sectorData, offsetInSector, data, 0, sectorData.Length - offsetInSector);
return ErrorNumber.NoError;
}
// Image sectors smaller than ADFS sectors - read multiple sectors
ulong imgSecPerAdfsSec = (uint)_blockSize / _imageSectorSize;
ulong startSector = adfsSector * imgSecPerAdfsSec + _partition.Start;
ErrorNumber err = _imagePlugin.ReadSectors(startSector, false, (uint)imgSecPerAdfsSec, out data, out _);
return err;
}
}

View File

@@ -105,30 +105,39 @@ public sealed partial class AcornADFS
// Each zone contains a bitstream where fragments are identified by their fragment ID
// Free space is tracked as a linked list starting at offset 8 in each zone
// For now, scan each zone and count free bits in the fragment list
int nzones = _discRecord.nzones + (_discRecord.nzones_high << 8);
if(nzones == 0) return 0;
if(_nzones == 0) return 0;
ulong totalFreeBits = 0;
int idlen = _discRecord.idlen;
int zoneSpare = _discRecord.zone_spare;
int sectorSize = 1 << _discRecord.log2secsize;
// Calculate bits per zone (excluding zone header and spare bits)
int bitsPerZone = sectorSize * 8 - zoneSpare;
// Start sector for first zone (after boot block for partitioned disks)
ulong zoneSector = _partition.Start;
for(var zone = 0; zone < nzones; zone++)
for(var zone = 0; zone < _nzones; zone++)
{
ErrorNumber errno = _imagePlugin.ReadSector(zoneSector + (ulong)zone, false, out byte[] zoneData, out _);
byte[] zoneData;
if(errno != ErrorNumber.NoError) continue;
// Use cached zone data if available
if(_mapCache != null && _mapCache[zone] != null)
zoneData = _mapCache[zone];
else
{
// Fall back to reading from disk
int map2blk = _discRecord.log2bpmb - _discRecord.log2secsize;
long mapAddr = (_nzones >> 1) * _zoneSize - (_nzones > 1 ? DISC_RECORD_SIZE * 8 : 0);
if(map2blk >= 0)
mapAddr <<= map2blk;
else
mapAddr >>= -map2blk;
ulong zoneSector = (ulong)(mapAddr / _blockSize) + (uint)zone;
ErrorNumber errno = ReadAdfsSector(zoneSector, out zoneData);
if(errno != ErrorNumber.NoError) continue;
}
// Scan the free list in this zone
totalFreeBits += ScanZoneFreeSpace(zoneData, idlen, bitsPerZone);
totalFreeBits += ScanZoneFreeSpace(zoneData, idlen, _zoneSize);
}
// Convert free bits to free blocks

View File

@@ -55,6 +55,17 @@ public sealed partial class AcornADFS
// RISC OS 12-bit filetype is stored in load_address[19:8] when bits[31:20] == 0xFFF
if(HasFiletype(entry.LoadAddr)) xattrs.Add(Xattrs.XATTR_ACORN_RISCOS_FILETYPE);
// Always expose full 32-bit attributes for ADFS-G/big directories, or 8-bit for standard
xattrs.Add(Xattrs.XATTR_ACORN_RISCOS_ATTRIBUTES);
// Expose load/exec addresses for unstamped files (pre-RISC OS or raw binary)
// These contain the actual load and execution addresses rather than filetype/timestamp
if(!HasFiletype(entry.LoadAddr))
{
xattrs.Add(Xattrs.XATTR_ACORN_RISCOS_LOAD_ADDR);
xattrs.Add(Xattrs.XATTR_ACORN_RISCOS_EXEC_ADDR);
}
return ErrorNumber.NoError;
}
@@ -78,6 +89,27 @@ public sealed partial class AcornADFS
buf = BitConverter.GetBytes(filetype);
return ErrorNumber.NoError;
case Xattrs.XATTR_ACORN_RISCOS_ATTRIBUTES:
// Return full 32-bit attributes for ADFS-G, or 8-bit zero-extended for standard
buf = BitConverter.GetBytes(entry.Attributes);
return ErrorNumber.NoError;
case Xattrs.XATTR_ACORN_RISCOS_LOAD_ADDR:
if(HasFiletype(entry.LoadAddr)) return ErrorNumber.NoSuchExtendedAttribute;
buf = BitConverter.GetBytes(entry.LoadAddr);
return ErrorNumber.NoError;
case Xattrs.XATTR_ACORN_RISCOS_EXEC_ADDR:
if(HasFiletype(entry.LoadAddr)) return ErrorNumber.NoSuchExtendedAttribute;
buf = BitConverter.GetBytes(entry.ExecAddr);
return ErrorNumber.NoError;
default:
return ErrorNumber.NoSuchExtendedAttribute;
}

View File

@@ -8,6 +8,15 @@ static class Xattrs
/// <summary>Extended attribute name for Acorn RISC OS filetype</summary>
public const string XATTR_ACORN_RISCOS_FILETYPE = "riscos.type";
/// <summary>Extended attribute name for Acorn RISC OS full 32-bit file attributes (ADFS-G/big directories)</summary>
public const string XATTR_ACORN_RISCOS_ATTRIBUTES = "riscos.attr";
/// <summary>Extended attribute name for Acorn RISC OS load address</summary>
public const string XATTR_ACORN_RISCOS_LOAD_ADDR = "riscos.loadaddr";
/// <summary>Extended attribute name for Acorn RISC OS exec address</summary>
public const string XATTR_ACORN_RISCOS_EXEC_ADDR = "riscos.execaddr";
/// <summary>Extended attribute name for the Amiga file comment</summary>
public const string XATTR_AMIGA_COMMENTS = "amiga.comments";