mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
243 lines
8.9 KiB
C#
243 lines
8.9 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : Mount.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Apple ProDOS 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 Aaru.CommonTypes.AaruMetadata;
|
|
using Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.Helpers;
|
|
using Aaru.Logging;
|
|
using Claunia.Encoding;
|
|
using Encoding = System.Text.Encoding;
|
|
using Marshal = Aaru.Helpers.Marshal;
|
|
using Partition = Aaru.CommonTypes.Partition;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
// Information from Apple ProDOS 8 Technical Reference
|
|
/// <inheritdoc />
|
|
public sealed partial class ProDOSPlugin
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
|
|
Dictionary<string, string> options, string @namespace)
|
|
{
|
|
_imagePlugin = imagePlugin;
|
|
_partition = partition;
|
|
_encoding = encoding ?? new Apple2c();
|
|
|
|
// ProDOS uses 512-byte blocks, but may be on 256-byte sector media
|
|
_multiplier = (uint)(imagePlugin.Info.SectorSize == 256 ? 2 : 1);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Mounting ProDOS volume");
|
|
AaruLogging.Debug(MODULE_NAME, "Sector size: {0}, multiplier: {1}", imagePlugin.Info.SectorSize, _multiplier);
|
|
|
|
// Read and validate the volume directory header
|
|
ErrorNumber errno = ReadVolumeDirectoryHeader();
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading volume directory header: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Volume name: {0}", _volumeName);
|
|
AaruLogging.Debug(MODULE_NAME, "Total blocks: {0}", _totalBlocks);
|
|
AaruLogging.Debug(MODULE_NAME, "Bitmap block: {0}", _bitmapBlock);
|
|
|
|
// Cache the root directory contents
|
|
errno = CacheRootDirectory();
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error caching root directory: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Cached {0} entries in root directory", _rootDirectoryCache.Count);
|
|
|
|
// Populate metadata
|
|
Metadata = new FileSystem
|
|
{
|
|
Type = FS_TYPE,
|
|
VolumeName = _volumeName,
|
|
Clusters = _totalBlocks,
|
|
ClusterSize = 512, // ProDOS always uses 512-byte blocks
|
|
Files = _volumeHeader.entry_count,
|
|
CreationDate = _creationTime
|
|
};
|
|
|
|
_mounted = true;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber Unmount()
|
|
{
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
_rootDirectoryCache?.Clear();
|
|
_rootDirectoryCache = null;
|
|
_volumeHeader = default(VolumeDirectoryHeader);
|
|
_volumeName = null;
|
|
_imagePlugin = null;
|
|
_encoding = null;
|
|
_mounted = false;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads and validates the volume directory header (superblock equivalent)</summary>
|
|
ErrorNumber ReadVolumeDirectoryHeader()
|
|
{
|
|
// Volume directory starts at block 2 (blocks 0 and 1 are boot code)
|
|
ErrorNumber errno = ReadBlock(2, out byte[] block);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Read block header (prev/next pointers) - first 4 bytes
|
|
_rootDirBlockHeader = Marshal.ByteArrayToStructureLittleEndian<DirectoryBlockHeader>(block);
|
|
|
|
// Read volume directory header - starts after block header at offset 4
|
|
_volumeHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<VolumeDirectoryHeader>(block,
|
|
4,
|
|
System.Runtime.InteropServices.Marshal
|
|
.SizeOf<VolumeDirectoryHeader>());
|
|
|
|
// Validate storage type (should be 0xF0 for volume directory header)
|
|
var storageType = (byte)((_volumeHeader.storage_type_name_length & STORAGE_TYPE_MASK) >> 4);
|
|
|
|
if(storageType != ROOT_DIRECTORY_TYPE)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Invalid storage type: 0x{0:X2}, expected 0x0F", storageType);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Validate entry length and entries per block
|
|
if(_volumeHeader.entry_length != ENTRY_LENGTH || _volumeHeader.entries_per_block != ENTRIES_PER_BLOCK)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Invalid directory format: entry_length={0}, entries_per_block={1}",
|
|
_volumeHeader.entry_length,
|
|
_volumeHeader.entries_per_block);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Extract volume name
|
|
var nameLength = (byte)(_volumeHeader.storage_type_name_length & NAME_LENGTH_MASK);
|
|
_volumeName = _encoding.GetString(_volumeHeader.volume_name, 0, nameLength);
|
|
|
|
// Apply case bits if present (GS/OS extension)
|
|
if((_volumeHeader.case_bits & 0x8000) != 0) _volumeName = ApplyCaseBits(_volumeName, _volumeHeader.case_bits);
|
|
|
|
// Parse creation time
|
|
_creationTime = DateHandlers.ProDosToDateTime(_volumeHeader.creation_date, _volumeHeader.creation_time);
|
|
|
|
_bitmapBlock = _volumeHeader.bitmap_block;
|
|
_totalBlocks = _volumeHeader.total_blocks;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Caches the root directory contents</summary>
|
|
ErrorNumber CacheRootDirectory()
|
|
{
|
|
_rootDirectoryCache = new Dictionary<string, CachedEntry>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Volume directory spans blocks 2-5 (4 blocks)
|
|
ushort currentBlock = 2;
|
|
var isFirstBlock = true;
|
|
var entriesRead = 0;
|
|
|
|
while(currentBlock != 0 && entriesRead < _volumeHeader.entry_count)
|
|
{
|
|
ErrorNumber errno = ReadBlock(currentBlock, out byte[] block);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Get next block pointer from header
|
|
var nextBlock = BitConverter.ToUInt16(block, 0x02);
|
|
|
|
// Determine entry start offset (skip header in first block)
|
|
var entryOffset = 0x04; // After prev/next pointers
|
|
|
|
if(isFirstBlock)
|
|
{
|
|
// First block has volume header, skip it
|
|
entryOffset += ENTRY_LENGTH; // Skip volume header (39 bytes)
|
|
isFirstBlock = false;
|
|
}
|
|
|
|
// Parse entries in this block
|
|
while(entryOffset + ENTRY_LENGTH <= 512)
|
|
{
|
|
// Check if entry is active
|
|
byte storageTypeNameLength = block[entryOffset];
|
|
|
|
if(storageTypeNameLength == 0)
|
|
{
|
|
entryOffset += ENTRY_LENGTH;
|
|
|
|
continue;
|
|
}
|
|
|
|
var storageType = (byte)((storageTypeNameLength & STORAGE_TYPE_MASK) >> 4);
|
|
|
|
// Skip deleted entries
|
|
if(storageType == EMPTY_STORAGE_TYPE)
|
|
{
|
|
entryOffset += ENTRY_LENGTH;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Parse the entry
|
|
CachedEntry entry = ParseDirectoryEntry(block, entryOffset);
|
|
|
|
if(entry != null && !string.IsNullOrEmpty(entry.Name))
|
|
{
|
|
_rootDirectoryCache[entry.Name] = entry;
|
|
entriesRead++;
|
|
}
|
|
|
|
entryOffset += ENTRY_LENGTH;
|
|
}
|
|
|
|
currentBlock = nextBlock;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
} |