mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-05-21 20:17:37 +00:00
[CP/M] Add support for analyze command
This commit is contained in:
386
Aaru.Filesystems/CPM/Analyze.cs
Normal file
386
Aaru.Filesystems/CPM/Analyze.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user