[BeFS] Implement directory operations.

This commit is contained in:
2026-02-02 19:38:48 +00:00
parent 1e41fa716a
commit 3ebdd0073c
7 changed files with 587 additions and 11 deletions

View File

@@ -57,6 +57,9 @@ public sealed partial class BeFS : IReadOnlyFilesystem
/// <summary>The filesystem superblock containing metadata about the volume</summary>
private SuperBlock _superblock;
/// <summary>Indicates if the filesystem is currently mounted</summary>
private bool _mounted;
/// <summary>Cache of root directory entries mapped from filename to i-node address</summary>
private readonly Dictionary<string, long> _rootDirectoryCache = new();

View File

@@ -232,8 +232,8 @@ public sealed partial class BeFS
(long)nodeData[valueIndex + 6] << 8 |
nodeData[valueIndex + 7];
// Extract the key name (null-terminated string)
string keyName = _encoding.GetString(nodeData, keyStart, keyLength).TrimEnd('\0');
// Extract the key name (fixed-length string, not null-terminated)
string keyName = _encoding.GetString(nodeData, keyStart, keyLength);
AaruLogging.Debug(MODULE_NAME,
"Entry {0}: name='{1}' ({2} bytes), i-node={3}",

302
Aaru.Filesystems/BFS/Dir.cs Normal file
View File

@@ -0,0 +1,302 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Dir.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : BeOS filesystem 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;
using Aaru.Helpers;
using Aaru.Logging;
using Marshal = Aaru.Helpers.Marshal;
namespace Aaru.Filesystems;
// Information from Practical Filesystem Design, ISBN 1-55860-497-9
/// <inheritdoc />
/// <summary>Implements detection of the Be (new) filesystem</summary>
public sealed partial class BeFS
{
/// <summary>Opens a directory for enumeration</summary>
/// <remarks>
/// Opens the specified directory path for enumeration. Traverses the directory tree
/// by reading i-nodes and B+tree structures to find subdirectories.
/// Returns a directory node containing all entries in the target directory.
/// </remarks>
/// <param name="path">Directory path to open (e.g., "/", "/home", "/home/user")</param>
/// <param name="node">Output directory node for enumeration</param>
/// <returns>Error code indicating success or failure</returns>
/// <inheritdoc />
public ErrorNumber OpenDir(string path, out IDirNode node)
{
node = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize the path
string normalizedPath = path ?? "/";
if(normalizedPath == "" || normalizedPath == ".") normalizedPath = "/";
// Root directory - return cached entries
if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
{
if(_rootDirectoryCache.Count == 0) return ErrorNumber.NoSuchFile;
node = new BefsDirNode
{
Path = "/",
Position = 0,
Entries = _rootDirectoryCache.Keys.ToArray()
};
return ErrorNumber.NoError;
}
// Subdirectory traversal
// Remove leading slash and split path
string pathWithoutLeadingSlash = normalizedPath.StartsWith("/", StringComparison.Ordinal)
? normalizedPath[1..]
: normalizedPath;
string[] pathComponents = pathWithoutLeadingSlash.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
if(pathComponents.Length == 0) return ErrorNumber.InvalidArgument;
AaruLogging.Debug(MODULE_NAME, "Traversing path with {0} components", pathComponents.Length);
// Start from root directory cache
Dictionary<string, long> currentEntries = _rootDirectoryCache;
bfs_inode currentInode = default;
var useRootInode = true;
// Traverse each path component
foreach(string component in pathComponents)
{
AaruLogging.Debug(MODULE_NAME, "Navigating to component '{0}'", component);
// Find the component in current directory
if(!currentEntries.TryGetValue(component, out long childInodeAddr))
{
AaruLogging.Debug(MODULE_NAME, "Component '{0}' not found in directory", component);
return ErrorNumber.NoSuchFile;
}
// Convert i-node address to block_run
// I-node address is stored as a block_run where allocation_group and start define the location
var ag = (uint)(childInodeAddr >> 32);
var blockOffset = (uint)(childInodeAddr & 0xFFFFFFFF);
var childInodeBlockRun = new block_run
{
allocation_group = ag,
start = (ushort)blockOffset,
len = 1
};
AaruLogging.Debug(MODULE_NAME, "Reading i-node at AG {0}, block {1}", ag, blockOffset);
// Read the child i-node
ErrorNumber errno = ReadInode(childInodeBlockRun, out bfs_inode childInode);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "Error reading child i-node: {0}", errno);
return errno;
}
// Check if it's a directory
if(!IsDirectory(childInode))
{
AaruLogging.Debug(MODULE_NAME, "Component '{0}' is not a directory", component);
return ErrorNumber.NotDirectory;
}
// Parse the child directory's B+tree
errno = ParseDirectoryBTree(childInode.data, out Dictionary<string, long> childEntries);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "Error parsing child directory B+tree: {0}", errno);
return errno;
}
currentEntries = childEntries;
currentInode = childInode;
useRootInode = false;
}
// Create directory node with the entries we found
var dirNode = new BefsDirNode
{
Path = normalizedPath,
Position = 0,
Entries = currentEntries.Keys.ToArray()
};
node = dirNode;
AaruLogging.Debug(MODULE_NAME,
"Successfully opened directory '{0}' with {1} entries",
normalizedPath,
dirNode.Entries.Length);
return ErrorNumber.NoError;
}
/// <summary>Closes a directory enumeration</summary>
/// <remarks>
/// Closes the directory node and cleans up resources used during enumeration.
/// Resets the position to mark the node as closed.
/// </remarks>
/// <param name="node">The directory node to close</param>
/// <returns>Error code indicating success or failure</returns>
/// <inheritdoc />
public ErrorNumber CloseDir(IDirNode node)
{
if(node is not BefsDirNode befsDirNode) return ErrorNumber.InvalidArgument;
befsDirNode.Position = -1;
befsDirNode.Entries = null;
return ErrorNumber.NoError;
}
/// <summary>Reads the next entry from a directory enumeration</summary>
/// <remarks>
/// Returns entries from the directory in the order they appear in the B+tree.
/// When all entries have been read, returns NoError with filename = null,
/// indicating end of directory.
/// </remarks>
/// <param name="node">The directory node to read from</param>
/// <param name="filename">Output filename of the next entry</param>
/// <returns>Error code indicating success or failure</returns>
/// <inheritdoc />
public ErrorNumber ReadDir(IDirNode node, out string filename)
{
filename = null;
if(!_mounted) return ErrorNumber.AccessDenied;
if(node is not BefsDirNode befsDirNode) return ErrorNumber.InvalidArgument;
if(befsDirNode.Position < 0) return ErrorNumber.InvalidArgument;
if(befsDirNode.Position >= befsDirNode.Entries.Length) return ErrorNumber.NoError;
filename = befsDirNode.Entries[befsDirNode.Position++];
return ErrorNumber.NoError;
}
/// <summary>Checks if an i-node represents a directory</summary>
private bool IsDirectory(bfs_inode inode) =>
// In BeFS, the mode field contains Unix-style permission bits
// S_IFDIR = 0x4000
(inode.mode & 0xF000) == 0x4000;
/// <summary>Parses a directory's B+tree and returns all entries</summary>
private ErrorNumber ParseDirectoryBTree(data_stream dataStream, out Dictionary<string, long> entries)
{
entries = [];
// Read B+tree header
ErrorNumber errno = ReadFromDataStream(dataStream, 0, 32, out byte[] headerData);
if(errno != ErrorNumber.NoError) return errno;
bt_header btHeader = _littleEndian
? Marshal.ByteArrayToStructureLittleEndian<bt_header>(headerData)
: Marshal.ByteArrayToStructureBigEndian<bt_header>(headerData);
if(btHeader.magic != BTREE_MAGIC) return ErrorNumber.InvalidArgument;
// Traverse B+tree to collect all entries
const long BEFS_BT_INVAL = unchecked((long)0xFFFFFFFFFFFFFFFF);
long nodeOffset = btHeader.node_root_pointer;
while(nodeOffset != 0 && nodeOffset != BEFS_BT_INVAL)
{
errno = ReadFromDataStream(dataStream, nodeOffset, btHeader.node_size, out byte[] nodeData);
if(errno != ErrorNumber.NoError) return errno;
bt_node_hdr nodeHeader = _littleEndian
? Marshal.ByteArrayToStructureLittleEndian<bt_node_hdr>(nodeData)
: Marshal.ByteArrayToStructureBigEndian<bt_node_hdr>(nodeData);
bool isLeafNode = nodeHeader.overflow_link == BEFS_BT_INVAL;
if(isLeafNode)
{
// Parse leaf node entries
const int headerSize = 28;
int keyDataStart = headerSize;
int keyDataEnd = keyDataStart + nodeHeader.keys_length;
int keyLengthIndexStart = keyDataEnd + 3 & ~3;
int valueDataStart = keyLengthIndexStart + nodeHeader.node_keys * 2;
var keyOffset = 0;
for(var i = 0; i < nodeHeader.node_keys; i++)
{
int offsetIndex = keyLengthIndexStart + i * 2;
ushort keyEndOffset = _littleEndian
? BitConverter.ToUInt16(nodeData, offsetIndex)
: BigEndianBitConverter.ToUInt16(nodeData, offsetIndex);
int keyStart = keyDataStart + keyOffset;
int keyLength = keyEndOffset - keyOffset;
int valueIndex = valueDataStart + i * 8;
long inodeNumber = _littleEndian
? BitConverter.ToInt64(nodeData, valueIndex)
: BigEndianBitConverter.ToInt64(nodeData, valueIndex);
string keyName = _encoding.GetString(nodeData, keyStart, keyLength);
if(!entries.ContainsKey(keyName)) keyOffset = keyEndOffset;
}
}
// Move to next sibling
if(nodeHeader.right_link != 0 && nodeHeader.right_link != BEFS_BT_INVAL)
nodeOffset = nodeHeader.right_link;
else
break;
}
return ErrorNumber.NoError;
}
}

View File

@@ -0,0 +1,230 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Inode.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : BeOS filesystem 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 Aaru.CommonTypes.Enums;
using Aaru.Helpers;
using Aaru.Logging;
using Marshal = Aaru.Helpers.Marshal;
namespace Aaru.Filesystems;
/// <inheritdoc />
/// <summary>Implements detection of the Be (new) filesystem</summary>
public sealed partial class BeFS
{
/// <summary>Reads an i-node from disk by its block_run address</summary>
/// <remarks>
/// Locates and reads an i-node from disk given its allocation group and block address.
/// Validates the i-node magic number to ensure correctness.
/// </remarks>
/// <param name="inodeAddr">The block_run address of the i-node to read</param>
/// <param name="inode">Output buffer containing the parsed i-node</param>
/// <returns>Error code indicating success or failure</returns>
private ErrorNumber ReadInode(block_run inodeAddr, out bfs_inode inode)
{
inode = default(bfs_inode);
// Calculate absolute block address using ag_shift
long blockAddress = ((long)inodeAddr.allocation_group << _superblock.ag_shift) + inodeAddr.start;
long byteAddressInFS = blockAddress * _superblock.block_size;
uint sectorSize = _imagePlugin.Info.SectorSize;
long partitionByteOffset = (long)_partition.Start * sectorSize;
long absoluteByteAddress = byteAddressInFS + partitionByteOffset;
long sectorAddress = absoluteByteAddress / sectorSize;
// Read the i-node block
ErrorNumber errno = _imagePlugin.ReadSectors((ulong)sectorAddress,
false,
(uint)((_superblock.inode_size + sectorSize - 1) / sectorSize),
out byte[] inodeData,
out SectorStatus[] _);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "Error reading i-node: {0}", errno);
return errno;
}
// Marshal the i-node
inode = _littleEndian
? Marshal.ByteArrayToStructureLittleEndian<bfs_inode>(inodeData)
: Marshal.ByteArrayToStructureBigEndian<bfs_inode>(inodeData);
// Validate i-node magic
if(inode.magic1 != INODE_MAGIC)
{
AaruLogging.Debug(MODULE_NAME,
"Invalid i-node magic! Expected 0x{0:X8}, got 0x{1:X8}",
INODE_MAGIC,
inode.magic1);
return ErrorNumber.InvalidArgument;
}
return ErrorNumber.NoError;
}
/// <summary>Finds a child entry in a directory and returns its i-node address</summary>
/// <remarks>
/// Searches the B+tree of a directory for a specific filename and returns
/// the i-node address associated with that entry.
/// </remarks>
/// <param name="dirInode">The i-node of the directory to search</param>
/// <param name="filename">The filename to search for</param>
/// <param name="childInodeAddr">Output i-node address of the child entry</param>
/// <returns>Error code indicating success or failure</returns>
private ErrorNumber FindDirectoryEntry(bfs_inode dirInode, string filename, out long childInodeAddr)
{
childInodeAddr = 0;
AaruLogging.Debug(MODULE_NAME, "Searching for entry '{0}' in directory", filename);
// Read the B+tree header
ErrorNumber errno = ReadFromDataStream(dirInode.data, 0, 32, out byte[] headerData);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "Error reading B+tree header from directory: {0}", errno);
return errno;
}
bt_header btHeader = _littleEndian
? Marshal.ByteArrayToStructureLittleEndian<bt_header>(headerData)
: Marshal.ByteArrayToStructureBigEndian<bt_header>(headerData);
if(btHeader.magic != BTREE_MAGIC)
{
AaruLogging.Debug(MODULE_NAME, "Invalid B+tree magic in directory");
return ErrorNumber.InvalidArgument;
}
// Search the B+tree for the filename
const long BEFS_BT_INVAL = unchecked((long)0xFFFFFFFFFFFFFFFF);
long nodeOffset = btHeader.node_root_pointer;
var found = false;
while(!found)
{
errno = ReadFromDataStream(dirInode.data, nodeOffset, btHeader.node_size, out byte[] nodeData);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "Error reading B+tree node: {0}", errno);
return errno;
}
bt_node_hdr nodeHeader = _littleEndian
? Marshal.ByteArrayToStructureLittleEndian<bt_node_hdr>(nodeData)
: Marshal.ByteArrayToStructureBigEndian<bt_node_hdr>(nodeData);
bool isLeafNode = nodeHeader.overflow_link == BEFS_BT_INVAL;
if(isLeafNode)
{
// Parse leaf node to find the entry
const int headerSize = 28;
int keyDataStart = headerSize;
int keyDataEnd = keyDataStart + nodeHeader.keys_length;
int keyLengthIndexStart = keyDataEnd + 3 & ~3;
int valueDataStart = keyLengthIndexStart + nodeHeader.node_keys * 2;
var keyOffset = 0;
for(var i = 0; i < nodeHeader.node_keys; i++)
{
int offsetIndex = keyLengthIndexStart + i * 2;
ushort keyEndOffset = _littleEndian
? BitConverter.ToUInt16(nodeData, offsetIndex)
: BigEndianBitConverter.ToUInt16(nodeData, offsetIndex);
int keyStart = keyDataStart + keyOffset;
int keyLength = keyEndOffset - keyOffset;
int valueIndex = valueDataStart + i * 8;
long inodeNumber = _littleEndian
? BitConverter.ToInt64(nodeData, valueIndex)
: BigEndianBitConverter.ToInt64(nodeData, valueIndex);
string keyName = _encoding.GetString(nodeData, keyStart, keyLength);
if(keyName == filename)
{
childInodeAddr = inodeNumber;
found = true;
AaruLogging.Debug(MODULE_NAME,
"Found entry '{0}' with i-node address {1}",
filename,
inodeNumber);
break;
}
keyOffset = keyEndOffset;
}
if(!found)
{
// Check right sibling
if(nodeHeader.right_link != 0 && nodeHeader.right_link != BEFS_BT_INVAL)
nodeOffset = nodeHeader.right_link;
else
{
AaruLogging.Debug(MODULE_NAME, "Entry '{0}' not found in directory", filename);
return ErrorNumber.NoSuchFile;
}
}
}
else
{
// Interior node - navigate to appropriate child
// For simplicity, start with root and follow overflow link
if(nodeHeader.overflow_link != 0 && nodeHeader.overflow_link != BEFS_BT_INVAL)
nodeOffset = nodeHeader.overflow_link;
else
{
AaruLogging.Debug(MODULE_NAME, "Interior node has no overflow link");
return ErrorNumber.InvalidArgument;
}
}
}
return ErrorNumber.NoError;
}
}

View File

@@ -0,0 +1,48 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Internal.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : BeOS filesystem 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;
/// <summary>Implements detection of the Be (new) filesystem</summary>
public sealed partial class BeFS
{
/// <summary>Directory node for enumerating directory contents</summary>
public sealed class BefsDirNode : IDirNode
{
/// <summary>Current position in the directory enumeration (entry index)</summary>
internal int Position { get; set; }
/// <summary>Array of directory entry names in this directory</summary>
internal string[] Entries { get; set; }
/// <inheritdoc />
public string Path { get; init; }
}
}

View File

@@ -115,6 +115,8 @@ public sealed partial class BeFS
Metadata.Dirty,
Metadata.FreeClusters);
_mounted = true;
return ErrorNumber.NoError;
}

View File

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