[ntfs] Implement directory operations.

This commit is contained in:
2026-03-01 14:30:28 +00:00
parent 207fee23db
commit 1e71e6a04a
4 changed files with 262 additions and 22 deletions

View File

@@ -0,0 +1,149 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Dir.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Microsoft NT 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
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class NTFS
{
/// <inheritdoc />
public ErrorNumber OpenDir(string path, out IDirNode node)
{
node = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize path
string normalizedPath = string.IsNullOrWhiteSpace(path) ? "/" : path;
if(!normalizedPath.StartsWith("/", StringComparison.Ordinal)) normalizedPath = "/" + normalizedPath;
// Root directory case
if(normalizedPath == "/")
{
if(_rootDirectoryCache == null) return ErrorNumber.InvalidArgument;
node = new NtfsDirNode
{
Path = "/",
Position = 0,
Entries = _rootDirectoryCache.Keys.OrderBy(static k => k, StringComparer.OrdinalIgnoreCase).ToArray()
};
return ErrorNumber.NoError;
}
// Parse path components
string cutPath = normalizedPath[1..]; // Remove leading '/'
string[] pieces = cutPath.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
if(pieces.Length == 0) return ErrorNumber.NoSuchFile;
// Start from root directory
Dictionary<string, ulong> currentDirectory = _rootDirectoryCache;
// Traverse through path components
for(var p = 0; p < pieces.Length; p++)
{
string component = pieces[p];
// Look for the component in current directory (case-insensitive via dictionary comparer)
if(!currentDirectory.TryGetValue(component, out ulong mftRef)) return ErrorNumber.NoSuchFile;
// Read directory entries for this MFT record
ErrorNumber errno =
ReadDirectoryEntries((uint)(mftRef & 0x0000FFFFFFFFFFFF), out Dictionary<string, ulong> dirEntries);
if(errno != ErrorNumber.NoError)
{
// If the entry is not a directory, return appropriate error
if(errno == ErrorNumber.NotDirectory && p < pieces.Length - 1) return ErrorNumber.NotDirectory;
if(errno == ErrorNumber.NotDirectory) return errno;
return errno;
}
// If this is the last component, we're opening this directory
if(p == pieces.Length - 1)
{
node = new NtfsDirNode
{
Path = normalizedPath,
Position = 0,
Entries = dirEntries.Keys.OrderBy(static k => k, StringComparer.OrdinalIgnoreCase).ToArray()
};
return ErrorNumber.NoError;
}
// Not the last component - move to next level
currentDirectory = dirEntries;
}
return ErrorNumber.NoSuchFile;
}
/// <inheritdoc />
public ErrorNumber CloseDir(IDirNode node)
{
if(node is not NtfsDirNode mynode) return ErrorNumber.InvalidArgument;
mynode.Position = -1;
mynode.Entries = null;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber ReadDir(IDirNode node, out string filename)
{
filename = null;
if(!_mounted) return ErrorNumber.AccessDenied;
if(node is not NtfsDirNode mynode) return ErrorNumber.InvalidArgument;
if(mynode.Position < 0) return ErrorNumber.InvalidArgument;
// End of directory
if(mynode.Position >= mynode.Entries.Length) return ErrorNumber.NoError;
// Get current filename and advance position
filename = mynode.Entries[mynode.Position++];
return ErrorNumber.NoError;
}
}

View File

@@ -38,11 +38,52 @@ namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class NTFS
{
/// <summary>Reads and caches the directory entries for an arbitrary directory given its MFT record number.</summary>
/// <param name="mftRecordNumber">MFT record number of the directory.</param>
/// <param name="entries">Output dictionary mapping file names to MFT references.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber ReadDirectoryEntries(uint mftRecordNumber, out Dictionary<string, ulong> entries)
{
entries = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
ErrorNumber errno = ReadMftRecord(mftRecordNumber, out byte[] recordData);
if(errno != ErrorNumber.NoError) return errno;
MftRecord header =
Marshal.ByteArrayToStructureLittleEndian<MftRecord>(recordData, 0, Marshal.SizeOf<MftRecord>());
if(header.magic != NtfsRecordMagic.File)
{
AaruLogging.Debug(MODULE_NAME, "MFT record {0} has invalid magic", mftRecordNumber);
return ErrorNumber.InvalidArgument;
}
// Check that this is a directory
if(!header.flags.HasFlag(MftRecordFlags.IsDirectory))
{
AaruLogging.Debug(MODULE_NAME, "MFT record {0} is not a directory", mftRecordNumber);
return ErrorNumber.NotDirectory;
}
return CacheDirectoryEntries(recordData, header, entries);
}
/// <summary>Caches the root directory entries from the $INDEX_ROOT attribute in the root MFT record.</summary>
/// <param name="recordData">Raw root directory MFT record data after USA fixup.</param>
/// <param name="header">Parsed MFT record header.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber CacheRootDirectory(byte[] recordData, in MftRecord header)
ErrorNumber CacheRootDirectory(byte[] recordData, in MftRecord header) =>
CacheDirectoryEntries(recordData, header, _rootDirectoryCache);
/// <summary>Caches directory entries from the $INDEX_ROOT attribute in an MFT record.</summary>
/// <param name="recordData">Raw directory MFT record data after USA fixup.</param>
/// <param name="header">Parsed MFT record header.</param>
/// <param name="cache">Target dictionary to populate with file names and MFT references.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber CacheDirectoryEntries(byte[] recordData, in MftRecord header, Dictionary<string, ulong> cache)
{
int offset = header.attrs_offset;
@@ -92,10 +133,11 @@ public sealed partial class NTFS
int entriesStart = indexHeaderOffset + (int)indexRoot.index.entries_offset;
int entriesEnd = indexHeaderOffset + (int)indexRoot.index.index_length;
ParseIndexEntries(recordData, entriesStart, entriesEnd);
ParseIndexEntries(recordData, entriesStart, entriesEnd, cache);
// If the index has sub-nodes (LARGE_INDEX), we also need to read $INDEX_ALLOCATION
if(indexRoot.index.flags.HasFlag(IndexHeaderFlags.LargeIndex)) CacheIndexAllocation(recordData, header);
if(indexRoot.index.flags.HasFlag(IndexHeaderFlags.LargeIndex))
CacheIndexAllocation(recordData, header, cache);
return ErrorNumber.NoError;
}
@@ -112,7 +154,8 @@ public sealed partial class NTFS
/// <param name="data">Buffer containing the index entries.</param>
/// <param name="start">Start offset of the first entry.</param>
/// <param name="end">End offset (exclusive) of the entries area.</param>
void ParseIndexEntries(byte[] data, int start, int end)
/// <param name="cache">Target dictionary to populate with file names and MFT references.</param>
void ParseIndexEntries(byte[] data, int start, int end, Dictionary<string, ulong> cache)
{
int pos = start;
@@ -155,7 +198,7 @@ public sealed partial class NTFS
// The MFT reference is the lower 48 bits of indexed_file_or_data
ulong mftRef = entryHeader.indexed_file_or_data & 0x0000FFFFFFFFFFFF;
_rootDirectoryCache.TryAdd(fileName, mftRef);
cache.TryAdd(fileName, mftRef);
}
}
}
@@ -171,7 +214,8 @@ public sealed partial class NTFS
/// </summary>
/// <param name="recordData">Raw root directory MFT record data after USA fixup.</param>
/// <param name="header">Parsed MFT record header.</param>
void CacheIndexAllocation(byte[] recordData, in MftRecord header)
/// <param name="cache">Target dictionary to populate with file names and MFT references.</param>
void CacheIndexAllocation(byte[] recordData, in MftRecord header, Dictionary<string, ulong> cache)
{
int offset = header.attrs_offset;
@@ -201,7 +245,7 @@ public sealed partial class NTFS
List<(long offset, long length)> dataRuns =
ParseDataRuns(recordData, runListOffset, offset + (int)attrLength);
ReadIndexBlocks(dataRuns);
ReadIndexBlocks(dataRuns, cache);
return;
}
@@ -250,9 +294,8 @@ public sealed partial class NTFS
// Sign-extend if negative
if(offsetSize > 0 && (data[offset + offsetSize - 1] & 0x80) != 0)
{
for(int i = offsetSize; i < 8; i++) runOffset |= (long)0xFF << i * 8;
}
for(int i = offsetSize; i < 8; i++)
runOffset |= (long)0xFF << i * 8;
offset += offsetSize;
@@ -271,9 +314,10 @@ public sealed partial class NTFS
return runs;
}
/// <summary>Reads index blocks from data runs and parses their entries into the root directory cache.</summary>
/// <summary>Reads index blocks from data runs and parses their entries into the directory cache.</summary>
/// <param name="dataRuns">List of (absolute cluster offset, length in clusters) tuples.</param>
void ReadIndexBlocks(List<(long offset, long length)> dataRuns)
/// <param name="cache">Target dictionary to populate with file names and MFT references.</param>
void ReadIndexBlocks(List<(long offset, long length)> dataRuns, Dictionary<string, ulong> cache)
{
foreach((long clusterOffset, long clusterLength) in dataRuns)
{
@@ -327,7 +371,7 @@ public sealed partial class NTFS
int entriesStart = indexHeaderOffset + (int)indexBlock.index.entries_offset;
int entriesEnd = indexHeaderOffset + (int)indexBlock.index.index_length;
ParseIndexEntries(blockData, entriesStart, entriesEnd);
ParseIndexEntries(blockData, entriesStart, entriesEnd, cache);
}
}
}

View File

@@ -0,0 +1,56 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Internal.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Microsoft NT 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
// ****************************************************************************/
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class NTFS
{
#region Nested type: NtfsDirNode
/// <summary>Directory node implementation for NTFS directory traversal.</summary>
sealed class NtfsDirNode : IDirNode
{
/// <summary>Array of cached directory entry file names.</summary>
internal string[] Entries;
/// <summary>Current position in the directory contents array.</summary>
internal int Position;
#region IDirNode Members
/// <inheritdoc />
public string Path { get; init; }
#endregion
}
#endregion
}

View File

@@ -60,13 +60,4 @@ public sealed partial class NTFS
/// <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();
}