mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
717 lines
28 KiB
C#
717 lines
28 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : File.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 Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Structs;
|
|
using Aaru.Helpers;
|
|
using Aaru.Logging;
|
|
|
|
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
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber OpenFile(string path, out IFileNode node)
|
|
{
|
|
node = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Get the file entry
|
|
ErrorNumber err = GetFileEntry(path, out CatalogEntry entry);
|
|
|
|
if(err != ErrorNumber.NoError) return err;
|
|
|
|
if(entry is not FileEntry fileEntry) return ErrorNumber.IsDirectory;
|
|
|
|
// We'll open the data fork by default
|
|
node = new HfsFileNode
|
|
{
|
|
Path = path,
|
|
Length = fileEntry.DataForkLogicalSize,
|
|
Offset = 0,
|
|
FileEntry = fileEntry,
|
|
ForkType = ForkType.Data,
|
|
FirstExtents = fileEntry.DataForkExtents,
|
|
AllExtents = null // Will be lazily loaded on first read if needed
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseFile(IFileNode node)
|
|
{
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
if(node is not HfsFileNode myNode) return ErrorNumber.InvalidArgument;
|
|
|
|
// Clear references
|
|
myNode.FileEntry = null;
|
|
myNode.AllExtents = null;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read)
|
|
{
|
|
read = 0;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
if(buffer is null || buffer.Length < length) return ErrorNumber.InvalidArgument;
|
|
|
|
if(node is not HfsFileNode myNode) return ErrorNumber.InvalidArgument;
|
|
|
|
// Clamp read length to remaining file size
|
|
if(myNode.Offset + length > myNode.Length) length = myNode.Length - myNode.Offset;
|
|
|
|
if(length <= 0) return ErrorNumber.NoError;
|
|
|
|
// Lazy load extents if not already loaded
|
|
if(myNode.AllExtents == null)
|
|
{
|
|
ErrorNumber extentErr = GetFileExtents(myNode.FileEntry.CNID,
|
|
myNode.ForkType,
|
|
myNode.FirstExtents,
|
|
out List<ExtDescriptor> allExtents);
|
|
|
|
if(extentErr != ErrorNumber.NoError) return extentErr;
|
|
|
|
myNode.AllExtents = allExtents ?? [];
|
|
}
|
|
|
|
if(myNode.AllExtents.Count == 0)
|
|
{
|
|
if(length > 0) return ErrorNumber.InvalidArgument;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Find the starting extent and offset within that extent
|
|
long bytesProcessed = 0;
|
|
long currentOffset = myNode.Offset;
|
|
long bytesToRead = length;
|
|
var bufferPos = 0;
|
|
|
|
foreach(ExtDescriptor extent in myNode.AllExtents)
|
|
{
|
|
if(extent.xdrNumABlks == 0) break;
|
|
|
|
uint extentSizeBytes = extent.xdrNumABlks * _mdb.drAlBlkSiz;
|
|
|
|
// Skip extents that are entirely before our current offset
|
|
if(currentOffset >= extentSizeBytes)
|
|
{
|
|
currentOffset -= extentSizeBytes;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Calculate how much to read from this extent
|
|
var offsetInExtent = (uint)currentOffset;
|
|
var toReadFromExtent = (uint)Math.Min(extentSizeBytes - offsetInExtent, bytesToRead);
|
|
|
|
// Calculate sector info for this extent
|
|
// HFS uses 512-byte sectors internally; extent.xdrStABN is in allocation blocks
|
|
ulong extentOffsetSector512 = (ulong)extent.xdrStABN * _mdb.drAlBlkSiz / 512 + offsetInExtent / 512;
|
|
|
|
// Convert to device sector address
|
|
HfsOffsetToDeviceSector(extentOffsetSector512, out ulong deviceSector, out uint byteOffset);
|
|
|
|
// Adjust byte offset for offset within the first extent
|
|
byteOffset += offsetInExtent % 512;
|
|
uint sectorCount = (toReadFromExtent + byteOffset + _sectorSize - 1) / _sectorSize;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadFile: Reading extent at block={0}, offset={1}, sectorCount={2}, toRead={3}",
|
|
extent.xdrStABN,
|
|
offsetInExtent,
|
|
sectorCount,
|
|
toReadFromExtent);
|
|
|
|
ErrorNumber readErr =
|
|
_imagePlugin.ReadSectors(deviceSector, false, sectorCount, out byte[] sectorData, out _);
|
|
|
|
if(readErr != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile: Failed to read sectors: {0}", readErr);
|
|
|
|
return readErr;
|
|
}
|
|
|
|
if(sectorData == null || sectorData.Length == 0)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile: Got empty sector data");
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Copy data from sector buffer to output buffer, accounting for offset
|
|
uint bytesToCopy = Math.Min(toReadFromExtent, (uint)(sectorData.Length - byteOffset));
|
|
|
|
Array.Copy(sectorData, (int)byteOffset, buffer, bufferPos, bytesToCopy);
|
|
|
|
bufferPos += (int)bytesToCopy;
|
|
bytesProcessed += bytesToCopy;
|
|
bytesToRead -= bytesToCopy;
|
|
|
|
// Reset offset for next extent
|
|
currentOffset = 0;
|
|
|
|
if(bytesToRead <= 0) break;
|
|
}
|
|
|
|
read = bytesProcessed;
|
|
myNode.Offset += bytesProcessed;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile: Read {0} bytes, new offset={1}", bytesProcessed, myNode.Offset);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber Stat(string path, out FileEntryInfo stat)
|
|
{
|
|
stat = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize path - accept both '/' and ':' as path separators
|
|
// readdir returns paths with ':' for display, we split on '/' first then convert ':' in components
|
|
string normalizedPath = string.IsNullOrEmpty(path) ? "/" : path;
|
|
|
|
// Root directory case
|
|
if(string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if(_rootDirectory.dirDirID != kRootCnid) return ErrorNumber.InvalidArgument;
|
|
|
|
FileAttributes attributes = FileAttributes.Directory;
|
|
|
|
// Translate Finder flags to file attributes (matches AppleMFS pattern)
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsAlias))
|
|
attributes |= FileAttributes.Alias;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasBundle))
|
|
attributes |= FileAttributes.Bundle;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasBeenInited))
|
|
attributes |= FileAttributes.HasBeenInited;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasCustomIcon))
|
|
attributes |= FileAttributes.HasCustomIcon;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasNoINITs))
|
|
attributes |= FileAttributes.HasNoINITs;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsInvisible))
|
|
attributes |= FileAttributes.Hidden;
|
|
|
|
if(_rootDirectory.dirFndrInfo.frXFlags.HasFlag(AppleCommon.ExtendedFinderFlags.kExtendedFlagIsImmutable))
|
|
attributes |= FileAttributes.Immutable;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsOnDesk))
|
|
attributes |= FileAttributes.IsOnDesk;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsShared))
|
|
attributes |= FileAttributes.Shared;
|
|
|
|
if(_rootDirectory.dirUsrInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsStationery))
|
|
attributes |= FileAttributes.Stationery;
|
|
|
|
stat = new FileEntryInfo
|
|
{
|
|
Attributes = attributes,
|
|
BlockSize = _mdb.drAlBlkSiz,
|
|
Inode = _rootDirectory.dirDirID,
|
|
Links = 1,
|
|
CreationTime = DateHandlers.MacToDateTime(_rootDirectory.dirCrDat),
|
|
LastWriteTime = DateHandlers.MacToDateTime(_rootDirectory.dirMdDat),
|
|
LastWriteTimeUtc = DateHandlers.MacToDateTime(_rootDirectory.dirMdDat),
|
|
AccessTime = DateHandlers.MacToDateTime(_rootDirectory.dirMdDat),
|
|
AccessTimeUtc = DateHandlers.MacToDateTime(_rootDirectory.dirMdDat),
|
|
BackupTime = DateHandlers.MacToDateTime(_rootDirectory.dirBkDat),
|
|
BackupTimeUtc = DateHandlers.MacToDateTime(_rootDirectory.dirBkDat)
|
|
};
|
|
|
|
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;
|
|
|
|
// Traverse through all but the last path component
|
|
for(var p = 0; p < pieces.Length - 1; p++)
|
|
{
|
|
string component = pieces[p];
|
|
|
|
// Convert colons to slashes in component (readdir returns display names with colons)
|
|
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;
|
|
|
|
// Load next directory level
|
|
ErrorNumber cacheErr = CacheDirectoryIfNeeded(currentDirCNID);
|
|
|
|
if(cacheErr != ErrorNumber.NoError) return cacheErr;
|
|
|
|
currentDirectory = GetDirectoryEntries(currentDirCNID);
|
|
|
|
if(currentDirectory == null) return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
// Now look for the final component
|
|
string lastComponent = pieces[pieces.Length - 1];
|
|
|
|
// Convert colons to slashes in component (readdir returns display names with colons)
|
|
lastComponent = lastComponent.Replace(":", "/");
|
|
|
|
KeyValuePair<string, CatalogEntry> finalEntry = default;
|
|
var foundFinal = false;
|
|
|
|
if(currentDirectory != null)
|
|
{
|
|
foreach(KeyValuePair<string, CatalogEntry> entry in currentDirectory)
|
|
{
|
|
if(string.Equals(entry.Key, lastComponent, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
finalEntry = foundFinal ? throw new InvalidOperationException("Duplicate file entries") : entry;
|
|
foundFinal = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!foundFinal) return ErrorNumber.NoSuchFile;
|
|
|
|
CatalogEntry finalCatalogEntry = finalEntry.Value;
|
|
|
|
// Populate stat info based on entry type
|
|
if(finalCatalogEntry is DirectoryEntry dirEntry)
|
|
{
|
|
FileAttributes attributes = FileAttributes.Directory;
|
|
|
|
// Translate Finder flags to file attributes (matches AppleMFS pattern)
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsAlias))
|
|
attributes |= FileAttributes.Alias;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasBundle))
|
|
attributes |= FileAttributes.Bundle;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasBeenInited))
|
|
attributes |= FileAttributes.HasBeenInited;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasCustomIcon))
|
|
attributes |= FileAttributes.HasCustomIcon;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kHasNoINITs))
|
|
attributes |= FileAttributes.HasNoINITs;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsInvisible))
|
|
attributes |= FileAttributes.Hidden;
|
|
|
|
if(dirEntry.ExtendedFinderInfo.frXFlags.HasFlag(AppleCommon.ExtendedFinderFlags.kExtendedFlagIsImmutable))
|
|
attributes |= FileAttributes.Immutable;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsOnDesk))
|
|
attributes |= FileAttributes.IsOnDesk;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsShared))
|
|
attributes |= FileAttributes.Shared;
|
|
|
|
if(dirEntry.FinderInfo.frFlags.HasFlag(AppleCommon.FinderFlags.kIsStationery))
|
|
attributes |= FileAttributes.Stationery;
|
|
|
|
stat = new FileEntryInfo
|
|
{
|
|
Attributes = attributes,
|
|
BlockSize = _mdb.drAlBlkSiz,
|
|
Inode = dirEntry.CNID,
|
|
Links = 1,
|
|
CreationTime = DateHandlers.MacToDateTime(dirEntry.CreationDate),
|
|
LastWriteTime = DateHandlers.MacToDateTime(dirEntry.ModificationDate),
|
|
LastWriteTimeUtc = DateHandlers.MacToDateTime(dirEntry.ModificationDate),
|
|
AccessTime = DateHandlers.MacToDateTime(dirEntry.ModificationDate),
|
|
AccessTimeUtc = DateHandlers.MacToDateTime(dirEntry.ModificationDate),
|
|
BackupTime = DateHandlers.MacToDateTime(dirEntry.BackupDate),
|
|
BackupTimeUtc = DateHandlers.MacToDateTime(dirEntry.BackupDate)
|
|
};
|
|
}
|
|
else if(finalCatalogEntry is FileEntry fileEntry)
|
|
{
|
|
// Use data fork size as file length
|
|
uint fileSize = fileEntry.DataForkLogicalSize;
|
|
|
|
FileAttributes attributes = FileAttributes.File;
|
|
|
|
// Translate Finder flags to file attributes (matches AppleMFS pattern)
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kIsAlias))
|
|
attributes |= FileAttributes.Alias;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kHasBundle))
|
|
attributes |= FileAttributes.Bundle;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kHasBeenInited))
|
|
attributes |= FileAttributes.HasBeenInited;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kHasCustomIcon))
|
|
attributes |= FileAttributes.HasCustomIcon;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kHasNoINITs))
|
|
attributes |= FileAttributes.HasNoINITs;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kIsInvisible))
|
|
attributes |= FileAttributes.Hidden;
|
|
|
|
if(fileEntry.ExtendedFinderInfo.fdXFlags.HasFlag(AppleCommon.ExtendedFinderFlags.kExtendedFlagIsImmutable))
|
|
attributes |= FileAttributes.Immutable;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kIsOnDesk))
|
|
attributes |= FileAttributes.IsOnDesk;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kIsShared))
|
|
attributes |= FileAttributes.Shared;
|
|
|
|
if(fileEntry.FinderInfo.fdFlags.HasFlag(AppleCommon.FinderFlags.kIsStationery))
|
|
attributes |= FileAttributes.Stationery;
|
|
|
|
if(!attributes.HasFlag(FileAttributes.Alias) &&
|
|
!attributes.HasFlag(FileAttributes.Bundle) &&
|
|
!attributes.HasFlag(FileAttributes.Stationery))
|
|
attributes |= FileAttributes.File;
|
|
|
|
stat = new FileEntryInfo
|
|
{
|
|
Attributes = attributes,
|
|
Blocks = (fileSize + _mdb.drAlBlkSiz - 1) / _mdb.drAlBlkSiz,
|
|
BlockSize = _mdb.drAlBlkSiz,
|
|
Length = fileSize,
|
|
Inode = fileEntry.CNID,
|
|
Links = 1,
|
|
CreationTime = DateHandlers.MacToDateTime(fileEntry.CreationDate),
|
|
LastWriteTime = DateHandlers.MacToDateTime(fileEntry.ModificationDate),
|
|
LastWriteTimeUtc = DateHandlers.MacToDateTime(fileEntry.ModificationDate),
|
|
AccessTime = DateHandlers.MacToDateTime(fileEntry.ModificationDate),
|
|
BackupTime = DateHandlers.MacToDateTime(fileEntry.BackupDate),
|
|
BackupTimeUtc = DateHandlers.MacToDateTime(fileEntry.BackupDate)
|
|
};
|
|
}
|
|
else
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Gets a catalog entry (file or directory) by path</summary>
|
|
/// <param name="path">Path to the file or directory</param>
|
|
/// <param name="entry">The catalog entry if found</param>
|
|
/// <returns>Error number</returns>
|
|
private ErrorNumber GetFileEntry(string path, out CatalogEntry entry)
|
|
{
|
|
entry = null;
|
|
|
|
// Convert colons back to forward slashes for internal path matching
|
|
// Mac OS filenames use colons for the path separator, so convert them back to our internal format
|
|
path = path.Replace(":", "/");
|
|
|
|
// Root directory case
|
|
if(string.Equals(path, "/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Return a pseudo-directory entry for root
|
|
entry = new DirectoryEntry
|
|
{
|
|
CNID = _rootDirectory.dirDirID,
|
|
ParentID = kRootParentCnid,
|
|
Type = kCatalogRecordTypeDirectory,
|
|
Valence = _rootDirectory.dirVal,
|
|
FinderInfo = _rootDirectory.dirUsrInfo,
|
|
ExtendedFinderInfo = _rootDirectory.dirFndrInfo,
|
|
CreationDate = _rootDirectory.dirCrDat,
|
|
ModificationDate = _rootDirectory.dirMdDat,
|
|
BackupDate = _rootDirectory.dirBkDat
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Parse path components
|
|
string cutPath = path.StartsWith("/", StringComparison.Ordinal) ? path[1..] : path;
|
|
|
|
string[] pieces = cutPath.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if(pieces.Length == 0) return ErrorNumber.NoSuchFile;
|
|
|
|
// Start from root directory
|
|
Dictionary<string, CatalogEntry> currentDirectory = _rootDirectoryCache;
|
|
|
|
// Traverse through all path components
|
|
for(var p = 0; p < pieces.Length; p++)
|
|
{
|
|
string component = pieces[p];
|
|
|
|
// Replace '/' with ':' in names for HFS path matching
|
|
// HFS uses ':' as path separator, so '/' in the search must be converted
|
|
component = component.Replace("/", ":");
|
|
|
|
// Look for the component in current directory
|
|
var found = false;
|
|
CatalogEntry catalogEntry = null;
|
|
|
|
if(currentDirectory != null)
|
|
{
|
|
foreach(KeyValuePair<string, CatalogEntry> dirEntry in currentDirectory)
|
|
{
|
|
if(!string.Equals(dirEntry.Key, component, StringComparison.OrdinalIgnoreCase)) continue;
|
|
|
|
catalogEntry = dirEntry.Value;
|
|
found = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!found) return ErrorNumber.NoSuchFile;
|
|
|
|
// Last component
|
|
if(p == pieces.Length - 1)
|
|
{
|
|
entry = catalogEntry;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Not last component - must be a directory
|
|
if(catalogEntry.Type != kCatalogRecordTypeDirectory) return ErrorNumber.NotDirectory;
|
|
|
|
// Update current directory info
|
|
uint currentDirCnid = catalogEntry.CNID;
|
|
|
|
// Load next directory level
|
|
ErrorNumber cacheErr = CacheDirectoryIfNeeded(currentDirCnid);
|
|
|
|
if(cacheErr != ErrorNumber.NoError) return cacheErr;
|
|
|
|
currentDirectory = GetDirectoryEntries(currentDirCnid);
|
|
|
|
if(currentDirectory == null) return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
/// <summary>Reads a file fork (data or resource) from the disk using extents information</summary>
|
|
/// <param name="fileEntry">The file entry containing fork information</param>
|
|
/// <param name="forkType">Type of fork to read (Data or Resource)</param>
|
|
/// <param name="logicalSize">Logical size of the fork to read</param>
|
|
/// <param name="firstExtents">The first 3 extent descriptors for this fork</param>
|
|
/// <param name="forkData">The fork data read from disk</param>
|
|
/// <returns>Error number</returns>
|
|
private ErrorNumber ReadFork(FileEntry fileEntry, ForkType forkType, uint logicalSize, ExtDataRec firstExtents,
|
|
out byte[] forkData)
|
|
{
|
|
forkData = null;
|
|
|
|
if(logicalSize == 0)
|
|
{
|
|
forkData = [];
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadFork: Reading {0} bytes of fork data (file CNID={1}, fork={2})",
|
|
logicalSize,
|
|
fileEntry.CNID,
|
|
forkType);
|
|
|
|
// Debug: Log initial extents
|
|
for(var i = 0; i < 3; i++)
|
|
{
|
|
if(firstExtents.xdr == null || i >= firstExtents.xdr.Length || firstExtents.xdr[i].xdrNumABlks == 0)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Initial extent {0}: empty", i);
|
|
|
|
break;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Initial extent {0}: startBlock={1}, numBlocks={2}",
|
|
i,
|
|
firstExtents.xdr[i].xdrStABN,
|
|
firstExtents.xdr[i].xdrNumABlks);
|
|
}
|
|
|
|
// Get all extents for this fork (may include overflow extents from B-Tree)
|
|
ErrorNumber extentErr =
|
|
GetFileExtents(fileEntry.CNID, forkType, firstExtents, out List<ExtDescriptor> allExtents);
|
|
|
|
if(extentErr != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Failed to get extents for file {0}: {1}", fileEntry.CNID, extentErr);
|
|
|
|
return extentErr;
|
|
}
|
|
|
|
if(allExtents == null || allExtents.Count == 0)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "No extents found for file {0}", fileEntry.CNID);
|
|
|
|
forkData = [];
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Got {0} total extents for file {1}", allExtents.Count, fileEntry.CNID);
|
|
|
|
forkData = new byte[logicalSize];
|
|
uint bytesRead = 0;
|
|
|
|
// Read each extent
|
|
foreach(ExtDescriptor extent in allExtents)
|
|
{
|
|
if(extent.xdrNumABlks == 0) break;
|
|
|
|
uint extentSize = extent.xdrNumABlks * _mdb.drAlBlkSiz;
|
|
uint toRead = Math.Min(extentSize, logicalSize - bytesRead);
|
|
|
|
// Read the allocation blocks for this extent
|
|
ulong extentOffsetSector512 = (ulong)extent.xdrStABN * _mdb.drAlBlkSiz / 512;
|
|
|
|
// Convert to device sector address
|
|
HfsOffsetToDeviceSector(extentOffsetSector512, out ulong deviceSector, out uint byteOffset);
|
|
uint sectorCnt = (toRead + byteOffset + _sectorSize - 1) / _sectorSize;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Reading extent: startBlock={0}, numBlocks={1}, size={2} bytes at sector {3}, sectorCnt={4}",
|
|
extent.xdrStABN,
|
|
extent.xdrNumABlks,
|
|
toRead,
|
|
deviceSector,
|
|
sectorCnt);
|
|
|
|
ErrorNumber readErr =
|
|
_imagePlugin.ReadSectors(deviceSector, false, sectorCnt, out byte[] extentData, out _);
|
|
|
|
if(readErr != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Failed to read extent data: {0}", readErr);
|
|
|
|
return readErr;
|
|
}
|
|
|
|
if(extentData == null || extentData.Length == 0)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Read returned null or empty data for extent");
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Account for sector offset when copying data
|
|
if(extentData.Length < (int)byteOffset + toRead)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Insufficient data read: got {0} bytes, expected at least {1}",
|
|
extentData.Length,
|
|
byteOffset + toRead);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
uint bytesToCopy = Math.Min(toRead, (uint)(extentData.Length - byteOffset));
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Read {0} bytes from extent, copying {1} bytes from offset {2}",
|
|
extentData.Length,
|
|
bytesToCopy,
|
|
byteOffset);
|
|
|
|
Array.Copy(extentData, (int)byteOffset, forkData, bytesRead, bytesToCopy);
|
|
|
|
bytesRead += bytesToCopy;
|
|
|
|
if(bytesRead >= logicalSize) break;
|
|
}
|
|
|
|
if(bytesRead < logicalSize)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Warning: Read only {0} bytes out of {1} bytes for fork of file {2}",
|
|
bytesRead,
|
|
logicalSize,
|
|
fileEntry.CNID);
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
} |