diff --git a/Aaru.Filesystems/NTFS/Dir.cs b/Aaru.Filesystems/NTFS/Dir.cs new file mode 100644 index 000000000..53ff1a88e --- /dev/null +++ b/Aaru.Filesystems/NTFS/Dir.cs @@ -0,0 +1,149 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Dir.cs +// Author(s) : Natalia Portillo +// +// 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 . +// +// ---------------------------------------------------------------------------- +// 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; + +/// +public sealed partial class NTFS +{ + /// + 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 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 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; + } + + /// + public ErrorNumber CloseDir(IDirNode node) + { + if(node is not NtfsDirNode mynode) return ErrorNumber.InvalidArgument; + + mynode.Position = -1; + mynode.Entries = null; + + return ErrorNumber.NoError; + } + + /// + 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; + } +} \ No newline at end of file diff --git a/Aaru.Filesystems/NTFS/Index.cs b/Aaru.Filesystems/NTFS/Index.cs index c422de983..aa75f33d0 100644 --- a/Aaru.Filesystems/NTFS/Index.cs +++ b/Aaru.Filesystems/NTFS/Index.cs @@ -38,11 +38,52 @@ namespace Aaru.Filesystems; /// public sealed partial class NTFS { + /// Reads and caches the directory entries for an arbitrary directory given its MFT record number. + /// MFT record number of the directory. + /// Output dictionary mapping file names to MFT references. + /// Error number indicating success or failure. + ErrorNumber ReadDirectoryEntries(uint mftRecordNumber, out Dictionary entries) + { + entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ErrorNumber errno = ReadMftRecord(mftRecordNumber, out byte[] recordData); + + if(errno != ErrorNumber.NoError) return errno; + + MftRecord header = + Marshal.ByteArrayToStructureLittleEndian(recordData, 0, Marshal.SizeOf()); + + 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); + } + /// Caches the root directory entries from the $INDEX_ROOT attribute in the root MFT record. /// Raw root directory MFT record data after USA fixup. /// Parsed MFT record header. /// Error number indicating success or failure. - ErrorNumber CacheRootDirectory(byte[] recordData, in MftRecord header) + ErrorNumber CacheRootDirectory(byte[] recordData, in MftRecord header) => + CacheDirectoryEntries(recordData, header, _rootDirectoryCache); + + /// Caches directory entries from the $INDEX_ROOT attribute in an MFT record. + /// Raw directory MFT record data after USA fixup. + /// Parsed MFT record header. + /// Target dictionary to populate with file names and MFT references. + /// Error number indicating success or failure. + ErrorNumber CacheDirectoryEntries(byte[] recordData, in MftRecord header, Dictionary 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 /// Buffer containing the index entries. /// Start offset of the first entry. /// End offset (exclusive) of the entries area. - void ParseIndexEntries(byte[] data, int start, int end) + /// Target dictionary to populate with file names and MFT references. + void ParseIndexEntries(byte[] data, int start, int end, Dictionary 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 /// /// Raw root directory MFT record data after USA fixup. /// Parsed MFT record header. - void CacheIndexAllocation(byte[] recordData, in MftRecord header) + /// Target dictionary to populate with file names and MFT references. + void CacheIndexAllocation(byte[] recordData, in MftRecord header, Dictionary 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; } - /// Reads index blocks from data runs and parses their entries into the root directory cache. + /// Reads index blocks from data runs and parses their entries into the directory cache. /// List of (absolute cluster offset, length in clusters) tuples. - void ReadIndexBlocks(List<(long offset, long length)> dataRuns) + /// Target dictionary to populate with file names and MFT references. + void ReadIndexBlocks(List<(long offset, long length)> dataRuns, Dictionary 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); } } } diff --git a/Aaru.Filesystems/NTFS/Internal.cs b/Aaru.Filesystems/NTFS/Internal.cs new file mode 100644 index 000000000..a1145e918 --- /dev/null +++ b/Aaru.Filesystems/NTFS/Internal.cs @@ -0,0 +1,56 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Internal.cs +// Author(s) : Natalia Portillo +// +// 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Filesystems; + +/// +public sealed partial class NTFS +{ +#region Nested type: NtfsDirNode + + /// Directory node implementation for NTFS directory traversal. + sealed class NtfsDirNode : IDirNode + { + /// Array of cached directory entry file names. + internal string[] Entries; + + /// Current position in the directory contents array. + internal int Position; + +#region IDirNode Members + + /// + public string Path { get; init; } + +#endregion + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Filesystems/NTFS/Unimplemented.cs b/Aaru.Filesystems/NTFS/Unimplemented.cs index 73b2df04f..4a0534f04 100644 --- a/Aaru.Filesystems/NTFS/Unimplemented.cs +++ b/Aaru.Filesystems/NTFS/Unimplemented.cs @@ -60,13 +60,4 @@ public sealed partial class NTFS /// public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read) => throw new NotImplementedException(); - - /// - public ErrorNumber OpenDir(string path, out IDirNode node) => throw new NotImplementedException(); - - /// - public ErrorNumber CloseDir(IDirNode node) => throw new NotImplementedException(); - - /// - public ErrorNumber ReadDir(IDirNode node, out string filename) => throw new NotImplementedException(); } \ No newline at end of file