mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
[ntfs] Implement directory operations.
This commit is contained in:
149
Aaru.Filesystems/NTFS/Dir.cs
Normal file
149
Aaru.Filesystems/NTFS/Dir.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
Aaru.Filesystems/NTFS/Internal.cs
Normal file
56
Aaru.Filesystems/NTFS/Internal.cs
Normal 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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user