mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
[HFS] Implement directory operations.
This commit is contained in:
@@ -35,6 +35,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Aaru.CommonTypes.Interfaces;
|
||||
using Aaru.CommonTypes.Structs;
|
||||
|
||||
namespace Aaru.Filesystems;
|
||||
|
||||
@@ -47,9 +48,15 @@ public sealed partial class AppleHFS : IReadOnlyFilesystem
|
||||
/// <summary>Module name for debugging</summary>
|
||||
const string MODULE_NAME = "HFS plugin";
|
||||
|
||||
/// <summary>Cached directory entries by CNID, each entry keyed by filename</summary>
|
||||
Dictionary<uint, Dictionary<string, CatalogEntry>> _directoryCaches;
|
||||
|
||||
/// <summary>Character encoding for filenames and volume name</summary>
|
||||
Encoding _encoding;
|
||||
|
||||
/// <summary>Cached filesystem information</summary>
|
||||
FileSystemInfo _fileSystemInfo;
|
||||
|
||||
/// <summary>Reference to the media image for sector I/O operations</summary>
|
||||
IMediaImage _imagePlugin;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
// ****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Aaru.CommonTypes.Enums;
|
||||
using Aaru.Helpers;
|
||||
using Aaru.Logging;
|
||||
@@ -566,10 +567,10 @@ public sealed partial class AppleHFS
|
||||
// B-Tree Navigation Strategy:
|
||||
// 1. Navigate from root to the appropriate leaf node for targetParentID
|
||||
// 2. Once at a leaf node, traverse BACKWARD using ndBLink to find the FIRST leaf node
|
||||
// that might contain entries with parentID = targetParentID
|
||||
// that contains entries with parentID = targetParentID
|
||||
// 3. Then traverse FORWARD through sibling nodes (using ndFLink)
|
||||
// 4. Extract ALL records with parentID = targetParentID from each leaf node
|
||||
// 5. Stop when we encounter entries with parentID > targetParentID consistently
|
||||
// 5. Stop when we encounter parentID > targetParentID
|
||||
|
||||
uint currentNode = bthdr.bthRoot;
|
||||
ushort entriesFound = 0;
|
||||
@@ -592,14 +593,14 @@ public sealed partial class AppleHFS
|
||||
System.Runtime.InteropServices.Marshal
|
||||
.SizeOf(typeof(NodeDescriptor)));
|
||||
|
||||
// If we've reached a leaf node, we'll process from here
|
||||
// If we've reached a leaf node, process from here
|
||||
if(nodeDesc.ndType == NodeType.ndLeafNode)
|
||||
{
|
||||
// We've found a leaf node that may contain targetParentID entries.
|
||||
// Now traverse backwards to find the FIRST leaf node that contains targetParentID entries
|
||||
// Traverse backward to find the FIRST leaf node containing targetParentID entries
|
||||
uint leafNode = currentNode;
|
||||
|
||||
// Traverse backward to find the start of targetParentID entries
|
||||
// Keep going back while we can
|
||||
while(leafNode != 0)
|
||||
{
|
||||
errno = ReadNode(leafNode, bthdr.bthNodeSize, out byte[] leafData);
|
||||
@@ -617,22 +618,47 @@ public sealed partial class AppleHFS
|
||||
System.Runtime.InteropServices.Marshal
|
||||
.SizeOf(typeof(NodeDescriptor)));
|
||||
|
||||
// Check if this leaf node contains any targetParentID entries
|
||||
if(!LeafContainsTargetParentID(leafData, bthdr.bthNodeSize, targetParentID))
|
||||
// Check if this leaf has records with parentID <= targetParentID
|
||||
if(!LeafHasTargetOrEarlier(leafData, bthdr.bthNodeSize, targetParentID))
|
||||
{
|
||||
// This leaf doesn't have our target, move to next
|
||||
if(leafDesc.ndFLink == 0) break;
|
||||
|
||||
leafNode = leafDesc.ndFLink;
|
||||
// This node is entirely after target, so move forward
|
||||
if(leafDesc.ndFLink != 0)
|
||||
leafNode = leafDesc.ndFLink;
|
||||
else
|
||||
break;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a leaf with targetParentID entries. Move back one more to make sure we're at the beginning
|
||||
// This leaf might contain our target, but check if we should go back more
|
||||
if(leafDesc.ndBLink != 0)
|
||||
leafNode = leafDesc.ndBLink;
|
||||
else
|
||||
break; // This is the first leaf node
|
||||
{
|
||||
uint prevNode = leafDesc.ndBLink;
|
||||
|
||||
// Peek at previous leaf to check if it has our parentID too
|
||||
errno = ReadNode(prevNode, bthdr.bthNodeSize, out byte[] prevData);
|
||||
|
||||
if(errno != ErrorNumber.NoError)
|
||||
{
|
||||
// Can't read previous, so process from here
|
||||
leafNode = leafNode;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if(LeafHasTargetOrEarlier(prevData, bthdr.bthNodeSize, targetParentID))
|
||||
{
|
||||
// Previous node has our target too, so go back
|
||||
leafNode = prevNode;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Previous node doesn't have our target, so this is the start
|
||||
}
|
||||
|
||||
// No previous link, this is the first leaf
|
||||
break;
|
||||
}
|
||||
|
||||
// Now traverse forward and extract all entries with targetParentID
|
||||
@@ -666,29 +692,36 @@ public sealed partial class AppleHFS
|
||||
return errno;
|
||||
}
|
||||
|
||||
// Check if next leaf node contains entries with targetParentID
|
||||
// Check if we've found all entries
|
||||
if(entriesFound >= expectedCount)
|
||||
{
|
||||
AaruLogging.Debug(MODULE_NAME, $"Found all expected entries: {entriesFound}/{expectedCount}");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Move to next leaf node
|
||||
if(leafDesc.ndFLink == 0) break; // Last leaf node reached
|
||||
|
||||
uint nextLeafNode = leafDesc.ndFLink;
|
||||
errno = ReadNode(nextLeafNode, bthdr.bthNodeSize, out byte[] nextLeafData);
|
||||
leafNode = leafDesc.ndFLink;
|
||||
|
||||
// Peek at next leaf to check if it still has targetParentID entries
|
||||
errno = ReadNode(leafNode, bthdr.bthNodeSize, out byte[] nextLeafData);
|
||||
|
||||
if(errno != ErrorNumber.NoError)
|
||||
{
|
||||
AaruLogging.Debug(MODULE_NAME, $"Failed to read next leaf node {nextLeafNode}");
|
||||
AaruLogging.Debug(MODULE_NAME, $"Failed to peek at next leaf node {leafNode}");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// If the next leaf doesn't contain targetParentID, we're done
|
||||
if(!LeafContainsTargetParentID(nextLeafData, bthdr.bthNodeSize, targetParentID))
|
||||
if(!LeafHasTargetParentID(nextLeafData, bthdr.bthNodeSize, targetParentID))
|
||||
{
|
||||
AaruLogging.Debug(MODULE_NAME,
|
||||
$"Next leaf node {nextLeafNode} doesn't contain parentID={targetParentID}, stopping traversal");
|
||||
$"Next leaf node {leafNode} doesn't contain parentID={targetParentID}, stopping");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
leafNode = nextLeafNode;
|
||||
}
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, $"Root directory caching complete: {entriesFound} entries found");
|
||||
@@ -713,7 +746,7 @@ public sealed partial class AppleHFS
|
||||
}
|
||||
|
||||
/// <summary>Checks if a leaf node contains any entries with a specific parentID</summary>
|
||||
bool LeafContainsTargetParentID(byte[] leafNode, ushort nodeSize, uint targetParentID)
|
||||
bool LeafHasTargetParentID(byte[] leafNode, ushort nodeSize, uint targetParentID)
|
||||
{
|
||||
NodeDescriptor nodeDesc =
|
||||
Marshal.ByteArrayToStructureBigEndian<NodeDescriptor>(leafNode,
|
||||
@@ -742,6 +775,37 @@ public sealed partial class AppleHFS
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Checks if a leaf node contains entries with parentID less than or equal to target</summary>
|
||||
bool LeafHasTargetOrEarlier(byte[] leafNode, ushort nodeSize, uint targetParentID)
|
||||
{
|
||||
NodeDescriptor nodeDesc =
|
||||
Marshal.ByteArrayToStructureBigEndian<NodeDescriptor>(leafNode,
|
||||
0,
|
||||
System.Runtime.InteropServices.Marshal
|
||||
.SizeOf(typeof(NodeDescriptor)));
|
||||
|
||||
if(nodeDesc.ndType != NodeType.ndLeafNode) return false;
|
||||
|
||||
if(nodeDesc.ndNRecs == 0) return false;
|
||||
|
||||
// Check first record (smallest parentID in this node)
|
||||
var offsetPos = (ushort)(nodeSize - 2);
|
||||
|
||||
if(offsetPos < 14 || offsetPos >= nodeSize) return false;
|
||||
|
||||
var recordOffset = BigEndianBitConverter.ToUInt16(leafNode, offsetPos);
|
||||
|
||||
if(recordOffset < 14 || recordOffset >= nodeSize) return false;
|
||||
|
||||
var minParentID = BigEndianBitConverter.ToUInt32(leafNode, recordOffset + 2);
|
||||
|
||||
return minParentID <= targetParentID;
|
||||
}
|
||||
|
||||
/// <summary>Checks if a leaf node contains any entries with a specific parentID</summary>
|
||||
bool LeafContainsTargetParentID(byte[] leafNode, ushort nodeSize, uint targetParentID) =>
|
||||
LeafHasTargetParentID(leafNode, nodeSize, targetParentID);
|
||||
|
||||
|
||||
/// <summary>Extracts all directory and file entries from a leaf node matching a given parent ID</summary>
|
||||
ErrorNumber ExtractDirectoryEntriesFromLeaf(byte[] leafNode, ushort nodeSize, uint targetParentID,
|
||||
@@ -907,4 +971,241 @@ public sealed partial class AppleHFS
|
||||
// Use StringHandlers.CToString for proper null-terminated string handling
|
||||
return StringHandlers.CToString(nameBytes, _encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches directory entries for a given CNID if not already cached.
|
||||
/// Reuses existing root directory cache if CNID is kRootCnid.
|
||||
/// </summary>
|
||||
/// <param name="cnid">Catalog Node ID of the directory to cache</param>
|
||||
/// <returns>Error status</returns>
|
||||
ErrorNumber CacheDirectoryIfNeeded(uint cnid)
|
||||
{
|
||||
// Initialize directory cache dictionary if needed
|
||||
_directoryCaches ??= new Dictionary<uint, Dictionary<string, CatalogEntry>>();
|
||||
|
||||
// Root directory is already cached in _rootDirectoryCache
|
||||
if(cnid == kRootCnid) return ErrorNumber.NoError;
|
||||
|
||||
// Check if already cached
|
||||
if(_directoryCaches.ContainsKey(cnid)) return ErrorNumber.NoError;
|
||||
|
||||
// Cache this directory
|
||||
return CacheDirectory(cnid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches all entries for a directory by its CNID.
|
||||
/// Searches the catalog B-Tree for all records with the given parentID.
|
||||
/// </summary>
|
||||
/// <param name="cnid">Catalog Node ID (used as parentID for records) of the directory to cache</param>
|
||||
/// <returns>Error status</returns>
|
||||
ErrorNumber CacheDirectory(uint cnid)
|
||||
{
|
||||
_directoryCaches ??= new Dictionary<uint, Dictionary<string, CatalogEntry>>();
|
||||
|
||||
// Read catalog header to get B-Tree information
|
||||
ErrorNumber errno = ReadCatalogHeader(out BTHdrRed bthdr);
|
||||
|
||||
if(errno != ErrorNumber.NoError) return errno;
|
||||
|
||||
var directoryEntries = new Dictionary<string, CatalogEntry>();
|
||||
|
||||
// Navigate B-Tree to find entries with parentID = cnid
|
||||
uint currentNode = bthdr.bthRoot;
|
||||
uint targetParentID = cnid;
|
||||
|
||||
// Traverse from root to leaf
|
||||
for(var level = 0; level < bthdr.bthDepth; level++)
|
||||
{
|
||||
errno = ReadNode(currentNode, bthdr.bthNodeSize, out byte[] nodeData);
|
||||
|
||||
if(errno != ErrorNumber.NoError) return errno;
|
||||
|
||||
NodeDescriptor nodeDesc =
|
||||
Marshal.ByteArrayToStructureBigEndian<NodeDescriptor>(nodeData,
|
||||
0,
|
||||
System.Runtime.InteropServices.Marshal
|
||||
.SizeOf(typeof(NodeDescriptor)));
|
||||
|
||||
// Reached a leaf node
|
||||
if(nodeDesc.ndType == NodeType.ndLeafNode)
|
||||
{
|
||||
// Extract all entries with parentID = cnid from this leaf and following siblings
|
||||
uint leafNode = currentNode;
|
||||
|
||||
while(leafNode != 0)
|
||||
{
|
||||
errno = ReadNode(leafNode, bthdr.bthNodeSize, out byte[] leafData);
|
||||
|
||||
if(errno != ErrorNumber.NoError) return errno;
|
||||
|
||||
NodeDescriptor leafDesc =
|
||||
Marshal.ByteArrayToStructureBigEndian<NodeDescriptor>(leafData,
|
||||
0,
|
||||
System.Runtime.InteropServices.Marshal
|
||||
.SizeOf(typeof(NodeDescriptor)));
|
||||
|
||||
// Extract entries from this leaf
|
||||
errno = ExtractDirectoryEntriesFromLeafForCnid(leafData,
|
||||
bthdr.bthNodeSize,
|
||||
targetParentID,
|
||||
directoryEntries);
|
||||
|
||||
if(errno != ErrorNumber.NoError) return errno;
|
||||
|
||||
// Check if next leaf contains entries for this parentID
|
||||
if(leafDesc.ndFLink == 0) break;
|
||||
|
||||
leafNode = leafDesc.ndFLink;
|
||||
errno = ReadNode(leafNode, bthdr.bthNodeSize, out byte[] nextLeafData);
|
||||
|
||||
if(errno != ErrorNumber.NoError) break;
|
||||
|
||||
if(!LeafContainsTargetParentID(nextLeafData, bthdr.bthNodeSize, targetParentID)) break;
|
||||
}
|
||||
|
||||
// Cache the directory
|
||||
_directoryCaches[cnid] = directoryEntries;
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
// Index node - find the child pointer for targetParentID
|
||||
errno = FindIndexPointer(nodeData, bthdr.bthNodeSize, targetParentID, out currentNode);
|
||||
|
||||
if(errno != ErrorNumber.NoError) return errno;
|
||||
}
|
||||
|
||||
return ErrorNumber.InvalidArgument;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts directory entries from a leaf node for a specific CNID.
|
||||
/// Similar to ExtractDirectoryEntriesFromLeaf but extracts to a provided dictionary.
|
||||
/// </summary>
|
||||
ErrorNumber ExtractDirectoryEntriesFromLeafForCnid(byte[] leafNode, ushort nodeSize, uint targetParentID,
|
||||
Dictionary<string, CatalogEntry> entries)
|
||||
{
|
||||
NodeDescriptor nodeDesc =
|
||||
Marshal.ByteArrayToStructureBigEndian<NodeDescriptor>(leafNode,
|
||||
0,
|
||||
System.Runtime.InteropServices.Marshal
|
||||
.SizeOf(typeof(NodeDescriptor)));
|
||||
|
||||
if(nodeDesc.ndType != NodeType.ndLeafNode) return ErrorNumber.InvalidArgument;
|
||||
|
||||
// Iterate through records in this leaf node
|
||||
for(ushort i = 0; i < nodeDesc.ndNRecs; i++)
|
||||
{
|
||||
var offsetPos = (ushort)(nodeSize - 2 - i * 2);
|
||||
|
||||
if(offsetPos < 14 || offsetPos >= nodeSize) continue;
|
||||
|
||||
var recordOffset = BigEndianBitConverter.ToUInt16(leafNode, offsetPos);
|
||||
|
||||
if(recordOffset < 14 || recordOffset >= nodeSize) continue;
|
||||
|
||||
// Parse the key
|
||||
byte keyLen = leafNode[recordOffset];
|
||||
var keyParentID = BigEndianBitConverter.ToUInt32(leafNode, recordOffset + 2);
|
||||
|
||||
// Stop if we've passed the target parentID
|
||||
if(keyParentID > targetParentID) break;
|
||||
|
||||
// Skip if not our target parentID
|
||||
if(keyParentID != targetParentID) continue;
|
||||
|
||||
// Extract the name
|
||||
byte nameLen = leafNode[recordOffset + 6];
|
||||
string name = ExtractCatalogName(leafNode, recordOffset + 7, nameLen);
|
||||
|
||||
if(string.IsNullOrEmpty(name)) continue;
|
||||
|
||||
// Key size (padded to word boundary)
|
||||
var keySize = (ushort)(keyLen + 1 + 1 & ~1);
|
||||
int dataOffset = recordOffset + keySize;
|
||||
|
||||
if(dataOffset >= leafNode.Length) continue;
|
||||
|
||||
byte recordType = leafNode[dataOffset];
|
||||
|
||||
CatalogEntry entry = null;
|
||||
|
||||
// Parse based on record type
|
||||
if(recordType == kCatalogRecordTypeDirectory)
|
||||
{
|
||||
int cdrDirRecSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(CdrDirRec));
|
||||
|
||||
if(dataOffset + cdrDirRecSize <= leafNode.Length)
|
||||
{
|
||||
CdrDirRec dirRec =
|
||||
Marshal.ByteArrayToStructureBigEndian<CdrDirRec>(leafNode, dataOffset, cdrDirRecSize);
|
||||
|
||||
entry = new DirectoryEntry
|
||||
{
|
||||
Name = name,
|
||||
CNID = dirRec.dirDirID,
|
||||
ParentID = keyParentID,
|
||||
Type = kCatalogRecordTypeDirectory,
|
||||
Valence = dirRec.dirVal,
|
||||
CreationDate = dirRec.dirCrDat,
|
||||
ModificationDate = dirRec.dirMdDat,
|
||||
BackupDate = dirRec.dirBkDat,
|
||||
FinderInfo = dirRec.dirUsrInfo,
|
||||
ExtendedFinderInfo = dirRec.dirFndrInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
else if(recordType == kCatalogRecordTypeFile)
|
||||
{
|
||||
int cdrFilRecSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(CdrFilRec));
|
||||
|
||||
if(dataOffset + cdrFilRecSize <= leafNode.Length)
|
||||
{
|
||||
CdrFilRec filRec =
|
||||
Marshal.ByteArrayToStructureBigEndian<CdrFilRec>(leafNode, dataOffset, cdrFilRecSize);
|
||||
|
||||
entry = new FileEntry
|
||||
{
|
||||
Name = name,
|
||||
CNID = filRec.filFlNum,
|
||||
ParentID = keyParentID,
|
||||
Type = kCatalogRecordTypeFile,
|
||||
FinderInfo = filRec.filUsrWds,
|
||||
ExtendedFinderInfo = filRec.filFndrInfo,
|
||||
DataForkLogicalSize = filRec.filLgLen,
|
||||
DataForkPhysicalSize = filRec.filPyLen,
|
||||
DataForkStartBlock = filRec.filStBlk,
|
||||
ResourceForkLogicalSize = filRec.filRLgLen,
|
||||
ResourceForkPhysicalSize = filRec.filRPyLen,
|
||||
ResourceForkStartBlock = filRec.filRStBlk,
|
||||
CreationDate = filRec.filCrDat,
|
||||
ModificationDate = filRec.filMdDat,
|
||||
BackupDate = filRec.filBkDat
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if(entry != null) entries[name] = entry;
|
||||
}
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached directory entries for a given CNID.
|
||||
/// Returns the root directory cache if CNID is kRootCnid.
|
||||
/// </summary>
|
||||
/// <param name="cnid">Catalog Node ID of the directory</param>
|
||||
/// <returns>Dictionary of entries keyed by filename, or null if not cached</returns>
|
||||
Dictionary<string, CatalogEntry> GetDirectoryEntries(uint cnid)
|
||||
{
|
||||
if(cnid == kRootCnid) return _rootDirectoryCache;
|
||||
|
||||
if(_directoryCaches == null) return null;
|
||||
|
||||
_directoryCaches.TryGetValue(cnid, out Dictionary<string, CatalogEntry> entries);
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
207
Aaru.Filesystems/AppleHFS/Dir.cs
Normal file
207
Aaru.Filesystems/AppleHFS/Dir.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
// /***************************************************************************
|
||||
// Aaru Data Preservation Suite
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Filename : Dir.cs
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// Component : Apple Hierarchical File System 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
|
||||
// ****************************************************************************/
|
||||
|
||||
// ReSharper disable UnusedType.Local
|
||||
|
||||
// ReSharper disable IdentifierTypo
|
||||
// ReSharper disable MemberCanBePrivate.Local
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Aaru.CommonTypes.Enums;
|
||||
using Aaru.CommonTypes.Interfaces;
|
||||
|
||||
namespace Aaru.Filesystems;
|
||||
|
||||
// Information from Inside Macintosh
|
||||
// https://developer.apple.com/legacy/library/documentation/mac/pdf/Files/File_Manager.pdf
|
||||
public sealed partial class AppleHFS
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens a directory for enumeration.
|
||||
/// Supports full path traversal including subdirectories.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber OpenDir(string path, out IDirNode node)
|
||||
{
|
||||
node = null;
|
||||
|
||||
if(!_mounted) return ErrorNumber.AccessDenied;
|
||||
|
||||
// Normalize path
|
||||
string normalizedPath = string.IsNullOrEmpty(path) ? "/" : path;
|
||||
|
||||
// Root directory case
|
||||
if(string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if(_rootDirectoryCache == null) return ErrorNumber.InvalidArgument;
|
||||
|
||||
var contents = _rootDirectoryCache.Keys.ToList();
|
||||
contents.Sort();
|
||||
|
||||
node = new HfsDirNode
|
||||
{
|
||||
Path = "/",
|
||||
Position = 0,
|
||||
Contents = contents.ToArray(),
|
||||
DirectoryCNID = kRootCnid
|
||||
};
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
// Parse path components
|
||||
string cutPath = normalizedPath.StartsWith("/", StringComparison.Ordinal)
|
||||
? normalizedPath[1..]
|
||||
: normalizedPath;
|
||||
|
||||
string[] pieces = cutPath.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if(pieces.Length == 0) return ErrorNumber.NoSuchFile;
|
||||
|
||||
// Start from root directory
|
||||
Dictionary<string, CatalogEntry> currentDirectory = _rootDirectoryCache;
|
||||
uint currentDirCNID = kRootCnid;
|
||||
var currentPath = "";
|
||||
|
||||
// Traverse through path components
|
||||
for(var p = 0; p < pieces.Length; p++)
|
||||
{
|
||||
string component = pieces[p];
|
||||
|
||||
// Replace ':' back to '/' in names
|
||||
component = component.Replace(":", "/");
|
||||
|
||||
// Look for the component in current directory
|
||||
KeyValuePair<string, CatalogEntry> foundEntry = default;
|
||||
var found = false;
|
||||
|
||||
if(currentDirectory != null)
|
||||
{
|
||||
foreach(KeyValuePair<string, CatalogEntry> entry in currentDirectory)
|
||||
{
|
||||
if(string.Equals(entry.Key, component, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foundEntry = entry;
|
||||
found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!found) return ErrorNumber.NoSuchFile;
|
||||
|
||||
CatalogEntry catalogEntry = foundEntry.Value;
|
||||
|
||||
// Check if it's a directory
|
||||
if(catalogEntry.Type != kCatalogRecordTypeDirectory) return ErrorNumber.NotDirectory;
|
||||
|
||||
// Update current directory info
|
||||
currentDirCNID = catalogEntry.CNID;
|
||||
currentPath = p == 0 ? pieces[0] : $"{currentPath}/{pieces[p]}";
|
||||
|
||||
// If this is the last component, we're opening this directory
|
||||
if(p == pieces.Length - 1)
|
||||
{
|
||||
// Try to get cached entries for this directory
|
||||
ErrorNumber cacheErr = CacheDirectoryIfNeeded(currentDirCNID);
|
||||
|
||||
if(cacheErr != ErrorNumber.NoError) return cacheErr;
|
||||
|
||||
// Get entries for this directory from cache
|
||||
Dictionary<string, CatalogEntry> dirEntries = GetDirectoryEntries(currentDirCNID);
|
||||
|
||||
if(dirEntries == null) return ErrorNumber.InvalidArgument;
|
||||
|
||||
var contents = dirEntries.Keys.ToList();
|
||||
contents.Sort();
|
||||
|
||||
node = new HfsDirNode
|
||||
{
|
||||
Path = normalizedPath,
|
||||
Position = 0,
|
||||
Contents = contents.ToArray(),
|
||||
DirectoryCNID = currentDirCNID
|
||||
};
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
// Not the last component - need to load next level
|
||||
ErrorNumber cacheNextErr = CacheDirectoryIfNeeded(currentDirCNID);
|
||||
|
||||
if(cacheNextErr != ErrorNumber.NoError) return cacheNextErr;
|
||||
|
||||
currentDirectory = GetDirectoryEntries(currentDirCNID);
|
||||
|
||||
if(currentDirectory == null) return ErrorNumber.NoSuchFile;
|
||||
}
|
||||
|
||||
return ErrorNumber.NoSuchFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a directory node and releases resources.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber CloseDir(IDirNode node)
|
||||
{
|
||||
if(node is not HfsDirNode mynode) return ErrorNumber.InvalidArgument;
|
||||
|
||||
mynode.Position = -1;
|
||||
mynode.Contents = null;
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next filename from an open directory.
|
||||
/// Returns NoError on success with filename set, or NoError with null filename at end of directory.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber ReadDir(IDirNode node, out string filename)
|
||||
{
|
||||
filename = null;
|
||||
|
||||
if(!_mounted) return ErrorNumber.AccessDenied;
|
||||
|
||||
if(node is not HfsDirNode mynode) return ErrorNumber.InvalidArgument;
|
||||
|
||||
if(mynode.Position < 0) return ErrorNumber.InvalidArgument;
|
||||
|
||||
// End of directory
|
||||
if(mynode.Position >= mynode.Contents.Length) return ErrorNumber.NoError;
|
||||
|
||||
// Get current filename and advance position
|
||||
filename = mynode.Contents[mynode.Position++];
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,31 @@
|
||||
// These are not part of the Inside Macintosh specification and are
|
||||
// implementation-specific to the Aaru HFS plugin.
|
||||
|
||||
using Aaru.CommonTypes.Interfaces;
|
||||
|
||||
namespace Aaru.Filesystems;
|
||||
|
||||
public sealed partial class AppleHFS
|
||||
{
|
||||
/// <summary>DirNode implementation for Apple HFS</summary>
|
||||
sealed class HfsDirNode : IDirNode
|
||||
{
|
||||
/// <summary>Array of sorted filenames in the directory</summary>
|
||||
internal string[] Contents;
|
||||
|
||||
/// <summary>CNID of this directory for caching purposes</summary>
|
||||
internal uint DirectoryCNID;
|
||||
/// <summary>Current position in the directory contents array</summary>
|
||||
internal int Position;
|
||||
|
||||
#region IDirNode Members
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Path { get; init; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Nested type: CatalogEntry
|
||||
|
||||
/// <summary>Represents a catalog entry (directory or file)</summary>
|
||||
|
||||
@@ -37,6 +37,7 @@ using System.Text;
|
||||
using Aaru.CommonTypes.AaruMetadata;
|
||||
using Aaru.CommonTypes.Enums;
|
||||
using Aaru.CommonTypes.Interfaces;
|
||||
using Aaru.CommonTypes.Structs;
|
||||
using Aaru.Helpers;
|
||||
using Aaru.Logging;
|
||||
using Partition = Aaru.CommonTypes.Partition;
|
||||
@@ -205,6 +206,9 @@ public sealed partial class AppleHFS
|
||||
return errno;
|
||||
}
|
||||
|
||||
// Initialize directory cache dictionary
|
||||
_directoryCaches = new Dictionary<uint, Dictionary<string, CatalogEntry>>();
|
||||
|
||||
// Attempt to read and validate the Catalog File (B-Tree)
|
||||
errno = ReadAndValidateCatalog();
|
||||
|
||||
@@ -238,6 +242,17 @@ public sealed partial class AppleHFS
|
||||
// Populate metadata from MDB
|
||||
PopulateMetadata();
|
||||
|
||||
// Initialize cached filesystem information
|
||||
_fileSystemInfo = new FileSystemInfo
|
||||
{
|
||||
Blocks = _mdb.drNmAlBlks,
|
||||
FilenameLength = 31,
|
||||
Files = (ulong)_mdb.drFilCnt + _mdb.drDirCnt,
|
||||
FreeBlocks = _mdb.drFreeBks,
|
||||
FreeFiles = 0, // HFS doesn't track free files separately
|
||||
PluginId = Id
|
||||
};
|
||||
|
||||
// Mark filesystem as mounted
|
||||
_mounted = true;
|
||||
|
||||
|
||||
@@ -47,17 +47,7 @@ public sealed partial class AppleHFS
|
||||
|
||||
if(!_mounted) return ErrorNumber.AccessDenied;
|
||||
|
||||
stat = new FileSystemInfo
|
||||
{
|
||||
Blocks = _mdb.drNmAlBlks,
|
||||
FilenameLength = 31,
|
||||
Files = _mdb.drFilCnt + _mdb.drDirCnt,
|
||||
FreeBlocks = _mdb.drFreeBks,
|
||||
PluginId = Id,
|
||||
Type = FS_TYPE
|
||||
};
|
||||
|
||||
stat.FreeFiles = 0; // HFS doesn't track free files separately
|
||||
stat = _fileSystemInfo;
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
@@ -80,13 +80,4 @@ public sealed partial class AppleHFS
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber OpenDir(string path, out IDirNode node) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber CloseDir(IDirNode node) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber ReadDir(IDirNode node, out string filename) => throw new NotImplementedException();
|
||||
}
|
||||
Reference in New Issue
Block a user