[CP/M] Add support for analyze command

This commit is contained in:
2026-04-10 23:35:34 +01:00
parent f493a7a7fe
commit 3cff10dce8
3 changed files with 394 additions and 5 deletions

View File

@@ -0,0 +1,386 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Analyze.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : CP/M filesystem plugin.
//
// --[ License ] --------------------------------------------------------------
//
// 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, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Marshal = Aaru.Helpers.Marshal;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class CPM
{
#region IReadOnlyFilesystem Members
/// <inheritdoc />
public ErrorNumber GetFilesWithAffectedSectors(IEnumerable<(ulong Start, ulong End)> sectorExtents,
out List<FileSectorInfo> files,
InitProgressHandler initProgress = null,
UpdateProgressHandler updateProgress = null,
PulseProgressHandler pulseProgress = null,
EndProgressHandler endProgress = null)
{
files = [];
if(!_mounted) return ErrorNumber.AccessDenied;
if(sectorExtents is null) return ErrorNumber.InvalidArgument;
if(_device is null || _workingDefinition is null || _dpb is null || _sectorMask is null)
return ErrorNumber.InvalidArgument;
List<(ulong Start, ulong End)> normalizedExtents = NormalizeAnalyzeExtents(sectorExtents);
if(normalizedExtents.Count == 0) return ErrorNumber.NoError;
try
{
initProgress?.Invoke();
pulseProgress?.Invoke("/");
ErrorNumber errno = AnalyzeDirectory(normalizedExtents, files, updateProgress);
return errno;
}
finally
{
endProgress?.Invoke();
}
}
#endregion
ErrorNumber AnalyzeDirectory(IReadOnlyList<(ulong Start, ulong End)> sectorExtents, List<FileSectorInfo> files,
UpdateProgressHandler updateProgress)
{
int dirOff;
int dirSectors = (_dpb.drm + 1) * 32 / _workingDefinition.bytesPerSector;
if(_workingDefinition.sofs > 0)
dirOff = _workingDefinition.sofs;
else
dirOff = _workingDefinition.ofs * _workingDefinition.sectorsPerTrack;
using MemoryStream dirMs = new();
for(var d = 0; d < dirSectors; d++)
{
ulong physicalSector = LogicalSectorToPhysicalSector((ulong)(d + dirOff));
if(physicalSector >= _device.Info.Sectors) return ErrorNumber.OutOfRange;
ErrorNumber errno = _device.ReadSector(physicalSector, false, out byte[] sector, out _);
if(errno != ErrorNumber.NoError) return errno;
if(_workingDefinition.complement)
for(var b = 0; b < sector.Length; b++)
sector[b] = (byte)(~sector[b] & 0xFF);
dirMs.Write(sector, 0, sector.Length);
}
Dictionary<string, Dictionary<int, List<ushort>>> fileExtents = ParseFileExtents(dirMs.ToArray());
string[] orderedFiles = fileExtents.Keys.OrderBy(static entry => entry, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
long maximum = orderedFiles.Length > 0 ? orderedFiles.Length : 1;
for(var i = 0; i < orderedFiles.Length; i++)
{
string path = "/" + orderedFiles[i];
updateProgress?.Invoke(path, i + 1, maximum);
AddOverlappingFile(path, fileExtents[orderedFiles[i]], sectorExtents, files);
}
return ErrorNumber.NoError;
}
Dictionary<string, Dictionary<int, List<ushort>>> ParseFileExtents(byte[] directory)
{
Dictionary<string, Dictionary<int, List<ushort>>> fileExtents = [];
int blockSize = 128 << _dpb.bsh;
ulong sectorCount = _partition.End >= _partition.Start ? _partition.End - _partition.Start + 1 : 0;
ulong allocationBlockCount = blockSize > 0 ? sectorCount * _device.Info.SectorSize / (ulong)blockSize : 0;
for(var dOff = 0; dOff < directory.Length; dOff += 32)
{
switch(directory[dOff] & 0x7F)
{
case < 0x10 when allocationBlockCount > 256:
{
DirectoryEntry16 entry =
Marshal.ByteArrayToStructureLittleEndian<DirectoryEntry16>(directory, dOff, 32);
AddDirectoryEntryExtents(in entry, fileExtents);
break;
}
case < 0x10:
{
DirectoryEntry entry =
Marshal.ByteArrayToStructureLittleEndian<DirectoryEntry>(directory, dOff, 32);
AddDirectoryEntryExtents(in entry, fileExtents);
break;
}
}
}
return fileExtents;
}
void AddDirectoryEntryExtents(in DirectoryEntry16 entry,
Dictionary<string, Dictionary<int, List<ushort>>> fileExtents)
{
var filenameBytes = (byte[])entry.filename.Clone();
var extensionBytes = (byte[])entry.extension.Clone();
var validEntry = true;
for(var i = 0; i < 8; i++)
{
filenameBytes[i] &= 0x7F;
validEntry &= filenameBytes[i] >= 0x20;
}
for(var i = 0; i < 3; i++)
{
extensionBytes[i] &= 0x7F;
validEntry &= extensionBytes[i] >= 0x20;
}
if(!validEntry) return;
string filename = Encoding.ASCII.GetString(filenameBytes).Trim();
string extension = Encoding.ASCII.GetString(extensionBytes).Trim();
int user = entry.statusUser & 0x0F;
if(user > 0) filename = $"{user:X1}:{filename}";
if(!string.IsNullOrEmpty(extension)) filename = filename + "." + extension;
filename = filename.Replace('/', '\u2215');
int entryNo = (32 * entry.extentCounter + entry.extentCounterHigh) / (_dpb.exm + 1);
if(!fileExtents.TryGetValue(filename, out Dictionary<int, List<ushort>> extentBlocks)) extentBlocks = [];
if(!extentBlocks.TryGetValue(entryNo, out List<ushort> blocks)) blocks = [];
foreach(ushort block in entry.allocations.Where(static blk => blk != 0 && blk != ushort.MaxValue))
if(!blocks.Contains(block))
blocks.Add(block);
extentBlocks[entryNo] = blocks;
fileExtents[filename] = extentBlocks;
}
void AddDirectoryEntryExtents(in DirectoryEntry entry,
Dictionary<string, Dictionary<int, List<ushort>>> fileExtents)
{
var filenameBytes = (byte[])entry.filename.Clone();
var extensionBytes = (byte[])entry.extension.Clone();
var validEntry = true;
for(var i = 0; i < 8; i++)
{
filenameBytes[i] &= 0x7F;
validEntry &= filenameBytes[i] >= 0x20;
}
for(var i = 0; i < 3; i++)
{
extensionBytes[i] &= 0x7F;
validEntry &= extensionBytes[i] >= 0x20;
}
if(!validEntry) return;
string filename = Encoding.ASCII.GetString(filenameBytes).Trim();
string extension = Encoding.ASCII.GetString(extensionBytes).Trim();
int user = entry.statusUser & 0x0F;
if(user > 0) filename = $"{user:X1}:{filename}";
if(!string.IsNullOrEmpty(extension)) filename = filename + "." + extension;
filename = filename.Replace('/', '\u2215');
int entryNo = (32 * entry.extentCounterHigh + entry.extentCounter) / (_dpb.exm + 1);
if(!fileExtents.TryGetValue(filename, out Dictionary<int, List<ushort>> extentBlocks)) extentBlocks = [];
if(!extentBlocks.TryGetValue(entryNo, out List<ushort> blocks)) blocks = [];
foreach(byte block in entry.allocations.Where(static blk => blk != 0))
if(!blocks.Contains(block))
blocks.Add(block);
extentBlocks[entryNo] = blocks;
fileExtents[filename] = extentBlocks;
}
void AddOverlappingFile(string path, IReadOnlyDictionary<int, List<ushort>> extentBlocks,
IReadOnlyList<(ulong Start, ulong End)> sectorExtents, List<FileSectorInfo> files)
{
if(extentBlocks.Count == 0) return;
List<(ulong Start, ulong End)> overlaps = FindOverlappingExtents(extentBlocks, sectorExtents);
if(overlaps.Count == 0) return;
ulong inode = extentBlocks.OrderBy(static entry => entry.Key)
.SelectMany(static entry => entry.Value)
.Select(static block => (ulong)block)
.FirstOrDefault();
files.Add(new FileSectorInfo
{
Path = path,
Inode = inode,
AffectedSectors = overlaps
});
}
List<(ulong Start, ulong End)> FindOverlappingExtents(IReadOnlyDictionary<int, List<ushort>> extentBlocks,
IReadOnlyList<(ulong Start, ulong End)> sectorExtents)
{
List<(ulong Start, ulong End)> overlaps = [];
foreach(KeyValuePair<int, List<ushort>> extent in extentBlocks.OrderBy(static entry => entry.Key))
{
foreach(ushort allocationBlock in extent.Value)
AddAllocationBlockOverlaps(allocationBlock, sectorExtents, overlaps);
}
return NormalizeAnalyzeExtents(overlaps);
}
void AddAllocationBlockOverlaps(ushort allocationBlock, IReadOnlyList<(ulong Start, ulong End)> sectorExtents,
List<(ulong Start, ulong End)> overlaps)
{
if(allocationBlock == 0) return;
int blockSize = 128 << _dpb.bsh;
uint sectorSize = _device.Info.SectorSize;
if(blockSize <= 0 || sectorSize == 0) return;
if(sectorSize > blockSize)
{
uint blocksPerSector = sectorSize / (uint)blockSize;
if(blocksPerSector == 0) return;
AddLogicalSectorOverlap(allocationBlock / blocksPerSector, sectorExtents, overlaps);
return;
}
if(sectorSize < blockSize)
{
uint sectorsPerBlock = (uint)blockSize / sectorSize;
if(sectorsPerBlock == 0) return;
ulong firstLogicalSector = (ulong)allocationBlock * sectorsPerBlock;
for(uint i = 0; i < sectorsPerBlock; i++)
AddLogicalSectorOverlap(firstLogicalSector + i, sectorExtents, overlaps);
return;
}
AddLogicalSectorOverlap(allocationBlock, sectorExtents, overlaps);
}
void AddLogicalSectorOverlap(ulong logicalSector, IReadOnlyList<(ulong Start, ulong End)> sectorExtents,
List<(ulong Start, ulong End)> overlaps)
{
ulong physicalSector = LogicalSectorToPhysicalSector(logicalSector);
AddExtentOverlaps(physicalSector, physicalSector, sectorExtents, overlaps);
}
ulong LogicalSectorToPhysicalSector(ulong logicalSector)
{
if(_sectorMask is null || _sectorMask.Length == 0) return _partition.Start + logicalSector;
return _partition.Start +
logicalSector / (ulong)_sectorMask.Length * (ulong)_sectorMask.Length +
(ulong)_sectorMask[(int)(logicalSector % (ulong)_sectorMask.Length)];
}
static void AddExtentOverlaps(ulong startSector, ulong endSector,
IReadOnlyList<(ulong Start, ulong End)> sectorExtents,
List<(ulong Start, ulong End)> overlaps)
{
foreach((ulong Start, ulong End) requestedExtent in sectorExtents)
{
if(requestedExtent.End < startSector || requestedExtent.Start > endSector) continue;
overlaps.Add((Math.Max(startSector, requestedExtent.Start), Math.Min(endSector, requestedExtent.End)));
}
}
static List<(ulong Start, ulong End)> NormalizeAnalyzeExtents(IEnumerable<(ulong Start, ulong End)> extents)
{
var orderedExtents = extents.Where(static extent => extent.End >= extent.Start)
.OrderBy(static extent => extent.Start)
.ThenBy(static extent => extent.End)
.ToList();
List<(ulong Start, ulong End)> normalizedExtents = [];
if(orderedExtents.Count == 0) return normalizedExtents;
(ulong Start, ulong End) currentExtent = orderedExtents[0];
for(var i = 1; i < orderedExtents.Count; i++)
{
if(orderedExtents[i].Start <= currentExtent.End + 1)
{
currentExtent.End = Math.Max(currentExtent.End, orderedExtents[i].End);
continue;
}
normalizedExtents.Add(currentExtent);
currentExtent = orderedExtents[i];
}
normalizedExtents.Add(currentExtent);
return normalizedExtents;
}
}

View File

@@ -37,6 +37,7 @@ using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using FileSystemInfo = Aaru.CommonTypes.Structs.FileSystemInfo;
using Partition = Aaru.CommonTypes.Partition;
namespace Aaru.Filesystems;
@@ -71,6 +72,8 @@ public sealed partial class CPM : IReadOnlyFilesystem
/// <summary>Timestamp in volume label for update</summary>
byte[] _labelUpdateDate;
bool _mounted;
/// <summary>The mounted partition</summary>
Partition _partition;
/// <summary>Cached file passwords</summary>
Dictionary<string, byte[]> _passwordCache;
/// <summary>Sector deinterleaving mask</summary>

View File

@@ -57,8 +57,9 @@ public sealed partial class CPM
public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
Dictionary<string, string> options, string @namespace)
{
_device = imagePlugin;
_encoding = encoding ?? Encoding.GetEncoding("IBM437");
_device = imagePlugin;
_partition = partition;
_encoding = encoding ?? Encoding.GetEncoding("IBM437");
// As the identification is so complex, just call Identify() and relay on its findings
if(!Identify(_device, partition) || !_cpmFound || _workingDefinition == null || _dpb == null)
@@ -165,9 +166,8 @@ public sealed partial class CPM
if(errno != ErrorNumber.NoError) return errno;
if(_workingDefinition.complement)
{
for(var b = 0; b < readSector.Length; b++) readSector[b] = (byte)(~readSector[b] & 0xFF);
}
for(var b = 0; b < readSector.Length; b++)
readSector[b] = (byte)(~readSector[b] & 0xFF);
deinterleavedSectors.Add((ulong)p, readSector);
}