mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 17:56:18 +00:00
298 lines
11 KiB
C#
298 lines
11 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : Dir.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 System.Linq;
|
|
using Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.Helpers;
|
|
using Marshal = Aaru.Helpers.Marshal;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
/// <inheritdoc />
|
|
public sealed partial class ProDOSPlugin
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber OpenDir(string path, out IDirNode node)
|
|
{
|
|
node = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize path
|
|
string normalizedPath = path ?? "/";
|
|
|
|
if(string.IsNullOrEmpty(normalizedPath) || normalizedPath == ".") normalizedPath = "/";
|
|
|
|
// Root directory
|
|
if(normalizedPath == "/")
|
|
{
|
|
// Read root directory contents
|
|
ErrorNumber errno = ReadDirectoryContents(2, true, out Dictionary<string, CachedEntry> entries);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
node = new ProDosDirNode
|
|
{
|
|
Path = normalizedPath,
|
|
Contents = entries.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase).ToArray(),
|
|
Position = 0,
|
|
DirectoryKeyBlock = 2
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Subdirectory - traverse path to find it
|
|
string[] pathComponents = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// Start from root directory cache
|
|
Dictionary<string, CachedEntry> currentDir = _rootDirectoryCache;
|
|
CachedEntry targetEntry = null;
|
|
|
|
for(var i = 0; i < pathComponents.Length; i++)
|
|
{
|
|
string component = pathComponents[i];
|
|
|
|
if(component is "." or "..") continue;
|
|
|
|
if(!currentDir.TryGetValue(component, out CachedEntry entry)) return ErrorNumber.NoSuchFile;
|
|
|
|
// Last component must be a directory
|
|
if(i == pathComponents.Length - 1)
|
|
{
|
|
if(!entry.IsDirectory) return ErrorNumber.NotDirectory;
|
|
|
|
targetEntry = entry;
|
|
|
|
break;
|
|
}
|
|
|
|
// Intermediate components must be directories
|
|
if(!entry.IsDirectory) return ErrorNumber.NotDirectory;
|
|
|
|
// Read this subdirectory's contents
|
|
ErrorNumber errno =
|
|
ReadDirectoryContents(entry.KeyBlock, false, out Dictionary<string, CachedEntry> subDir);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
currentDir = subDir;
|
|
}
|
|
|
|
if(targetEntry == null) return ErrorNumber.NoSuchFile;
|
|
|
|
// Read the target directory contents
|
|
ErrorNumber err =
|
|
ReadDirectoryContents(targetEntry.KeyBlock, false, out Dictionary<string, CachedEntry> dirContents);
|
|
|
|
if(err != ErrorNumber.NoError) return err;
|
|
|
|
node = new ProDosDirNode
|
|
{
|
|
Path = normalizedPath,
|
|
Contents = dirContents.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase).ToArray(),
|
|
Position = 0,
|
|
DirectoryKeyBlock = targetEntry.KeyBlock
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseDir(IDirNode node)
|
|
{
|
|
if(node is not ProDosDirNode proDosNode) return ErrorNumber.InvalidArgument;
|
|
|
|
proDosNode.Contents = null;
|
|
proDosNode.Position = -1;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadDir(IDirNode node, out string filename)
|
|
{
|
|
filename = null;
|
|
|
|
if(node is not ProDosDirNode proDosNode) return ErrorNumber.InvalidArgument;
|
|
|
|
if(proDosNode.Position < 0 || proDosNode.Contents == null) return ErrorNumber.InvalidArgument;
|
|
|
|
if(proDosNode.Position >= proDosNode.Contents.Length) return ErrorNumber.NoError;
|
|
|
|
filename = proDosNode.Contents[proDosNode.Position++];
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads directory contents from a key block</summary>
|
|
/// <param name="keyBlock">Key block of the directory</param>
|
|
/// <param name="isVolumeDirectory">True if this is the volume directory (block 2)</param>
|
|
/// <param name="entries">Output dictionary of cached entries</param>
|
|
/// <returns>Error number</returns>
|
|
ErrorNumber ReadDirectoryContents(ushort keyBlock, bool isVolumeDirectory,
|
|
out Dictionary<string, CachedEntry> entries)
|
|
{
|
|
entries = new Dictionary<string, CachedEntry>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
ushort currentBlock = keyBlock;
|
|
ushort prevBlock = 0;
|
|
ushort entryCount = 0;
|
|
var entriesRead = 0;
|
|
|
|
// Entry index within block: 0 = header, 1-12 = entries (13 total slots per block)
|
|
// In first block, slot 0 is the directory/volume header
|
|
// In subsequent blocks, slot 0 is a regular entry
|
|
var currentEntry = 1; // Start at entry 1 (skip header in first block)
|
|
|
|
while(currentBlock != 0)
|
|
{
|
|
ErrorNumber errno = ReadBlock(currentBlock, out byte[] block);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
return errno;
|
|
|
|
// Read block header
|
|
DirectoryBlockHeader blockHeader = Marshal.ByteArrayToStructureLittleEndian<DirectoryBlockHeader>(block);
|
|
|
|
// Validate prev pointer matches (like Linux driver does)
|
|
if(blockHeader.prev != prevBlock)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
// First block has directory header at entry slot 0, read it to get entry count
|
|
if(currentBlock == keyBlock)
|
|
{
|
|
if(isVolumeDirectory)
|
|
{
|
|
VolumeDirectoryHeader volHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<VolumeDirectoryHeader>(block,
|
|
4,
|
|
System.Runtime.InteropServices.Marshal.SizeOf<VolumeDirectoryHeader>());
|
|
|
|
entryCount = volHeader.entry_count;
|
|
}
|
|
else
|
|
{
|
|
DirectoryHeader dirHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<DirectoryHeader>(block,
|
|
4,
|
|
System.Runtime.InteropServices.Marshal.SizeOf<DirectoryHeader>());
|
|
|
|
entryCount = dirHeader.entry_count;
|
|
}
|
|
}
|
|
|
|
// Parse entries in this block
|
|
// Each block has 13 entry slots (4 byte header + 13 * 39 byte entries = 511 bytes)
|
|
while(currentEntry < ENTRIES_PER_BLOCK)
|
|
{
|
|
// Calculate entry offset: 4 bytes header + entry index * 39 bytes
|
|
int entryOffset = 4 + currentEntry * ENTRY_LENGTH;
|
|
|
|
// Check if entry is active
|
|
byte storageTypeNameLength = block[entryOffset];
|
|
var storageType = (byte)((storageTypeNameLength & STORAGE_TYPE_MASK) >> 4);
|
|
|
|
// Skip deleted entries (storage type 0) and header entries
|
|
if(storageType != EMPTY_STORAGE_TYPE &&
|
|
storageType != SUBDIRECTORY_HEADER_TYPE &&
|
|
storageType != ROOT_DIRECTORY_TYPE)
|
|
{
|
|
// Parse the entry
|
|
CachedEntry entry = ParseDirectoryEntry(block, entryOffset);
|
|
|
|
if(entry != null && !string.IsNullOrEmpty(entry.Name))
|
|
{
|
|
entries[entry.Name] = entry;
|
|
entriesRead++;
|
|
}
|
|
}
|
|
|
|
currentEntry++;
|
|
|
|
// Stop if we've read all entries
|
|
if(entriesRead >= entryCount)
|
|
break;
|
|
}
|
|
|
|
// Stop if we've read all entries
|
|
if(entriesRead >= entryCount)
|
|
break;
|
|
|
|
// Move to next block
|
|
prevBlock = currentBlock;
|
|
currentBlock = blockHeader.next;
|
|
currentEntry = 0; // In subsequent blocks, start at entry 0
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Parses a directory entry from a block using marshalling</summary>
|
|
CachedEntry ParseDirectoryEntry(byte[] block, int offset)
|
|
{
|
|
// Marshal the entry structure
|
|
Entry entry =
|
|
Marshal.ByteArrayToStructureLittleEndian<Entry>(block,
|
|
offset,
|
|
System.Runtime.InteropServices.Marshal.SizeOf<Entry>());
|
|
|
|
var storageType = (byte)((entry.storage_type_name_length & STORAGE_TYPE_MASK) >> 4);
|
|
var nameLength = (byte)(entry.storage_type_name_length & NAME_LENGTH_MASK);
|
|
|
|
if(nameLength == 0) return null;
|
|
|
|
// Extract filename
|
|
string fileName = _encoding.GetString(entry.file_name, 0, nameLength);
|
|
|
|
// Apply case bits if present (GS/OS extension)
|
|
if((entry.case_bits & 0x8000) != 0) fileName = ApplyCaseBits(fileName, entry.case_bits);
|
|
|
|
// Parse EOF (3 bytes little-endian)
|
|
var eof = (uint)(entry.eof[0] | entry.eof[1] << 8 | entry.eof[2] << 16);
|
|
|
|
return new CachedEntry
|
|
{
|
|
Name = fileName,
|
|
StorageType = storageType,
|
|
FileType = entry.file_type,
|
|
KeyBlock = entry.key_pointer,
|
|
BlocksUsed = entry.blocks_used,
|
|
Eof = eof,
|
|
CreationTime = DateHandlers.ProDosToDateTime(entry.creation_date, entry.creation_time),
|
|
ModificationTime = DateHandlers.ProDosToDateTime(entry.modification_date, entry.modification_time),
|
|
Access = entry.access,
|
|
AuxType = entry.aux_type,
|
|
HeaderPointer = entry.header_pointer,
|
|
CaseBits = entry.case_bits
|
|
};
|
|
}
|
|
} |