Files
Aaru-aaru-dps/Aaru.Filesystems/SFS/File.cs

589 lines
22 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : File.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : SmartFileSystem 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 Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Helpers;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class SFS
{
/// <summary>Amiga epoch: January 1, 1978</summary>
static readonly DateTime _amigaEpoch = new(1978, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <inheritdoc />
public ErrorNumber ReadLink(string path, out string dest)
{
dest = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize the path
string normalizedPath = path ?? "";
if(normalizedPath == "" || normalizedPath == "." || normalizedPath == "/") return ErrorNumber.InvalidArgument;
// Get file stat to validate it's a symlink
ErrorNumber errno = Stat(normalizedPath, out FileEntryInfo stat);
if(errno != ErrorNumber.NoError) return errno;
// Verify it's a symbolic link
if(!stat.Attributes.HasFlag(FileAttributes.Symlink)) return ErrorNumber.InvalidArgument;
// Get the object node for this path
errno = GetObjectNodeForPath(normalizedPath, out uint objectNode);
if(errno != ErrorNumber.NoError) return errno;
// Find the object container for this node
errno = FindObjectNode(objectNode, out uint objectBlock);
if(errno != ErrorNumber.NoError) return errno;
errno = ReadBlock(objectBlock, out byte[] objectData);
if(errno != ErrorNumber.NoError) return errno;
// Find the object in the container
errno = FindObjectInContainer(objectData, objectNode, out int objectOffset);
if(errno != ErrorNumber.NoError) return errno;
// Read the softlink block pointer (data field at offset 12)
// For soft links, the data field points to a SOFTLINK_ID block
var softLinkBlock = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 12);
if(softLinkBlock == 0) return ErrorNumber.InvalidArgument;
// Read the soft link block
errno = ReadBlock(softLinkBlock, out byte[] linkData);
if(errno != ErrorNumber.NoError) return errno;
// Validate block ID
var blockId = BigEndianBitConverter.ToUInt32(linkData, 0);
if(blockId != SOFTLINK_ID) return ErrorNumber.InvalidArgument;
// SoftLink structure: header (12) + parent (4) + next (4) + previous (4) = 24 bytes
// Followed by null-terminated path string
const int stringOffset = 24;
// Read null-terminated string
var pathBytes = new List<byte>();
for(int i = stringOffset; i < linkData.Length && linkData[i] != 0; i++) pathBytes.Add(linkData[i]);
if(pathBytes.Count == 0) return ErrorNumber.InvalidArgument;
dest = _encoding.GetString(pathBytes.ToArray());
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber OpenFile(string path, out IFileNode node)
{
node = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize the path
string normalizedPath = path ?? "";
if(normalizedPath == "" || normalizedPath == "." || normalizedPath == "/") return ErrorNumber.IsDirectory;
// Get file stat to validate it's a file and get its size
ErrorNumber errno = Stat(normalizedPath, out FileEntryInfo stat);
if(errno != ErrorNumber.NoError) return errno;
// Verify it's a regular file, not a directory
if(stat.Attributes.HasFlag(FileAttributes.Directory)) return ErrorNumber.IsDirectory;
// Get the object node for this path
errno = GetObjectNodeForPath(normalizedPath, out uint objectNode);
if(errno != ErrorNumber.NoError) return errno;
// Find the object container for this node
errno = FindObjectNode(objectNode, out uint objectBlock);
if(errno != ErrorNumber.NoError) return errno;
errno = ReadBlock(objectBlock, out byte[] objectData);
if(errno != ErrorNumber.NoError) return errno;
// Find the object in the container
errno = FindObjectInContainer(objectData, objectNode, out int objectOffset);
if(errno != ErrorNumber.NoError) return errno;
// Read the first extent block (data field at offset 12)
var firstExtent = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 12);
// Create file node
node = new SfsFileNode
{
Path = normalizedPath,
Length = stat.Length,
Offset = 0,
FirstExtent = firstExtent,
CurrentExtent = firstExtent,
ExtentOffset = 0
};
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber CloseFile(IFileNode node)
{
if(!_mounted) return ErrorNumber.AccessDenied;
if(node is not SfsFileNode) return ErrorNumber.InvalidArgument;
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 SfsFileNode sfsNode) return ErrorNumber.InvalidArgument;
if(sfsNode.Offset < 0 || length < 0) return ErrorNumber.InvalidArgument;
// If at or past end of file, return zero bytes read
if(sfsNode.Offset >= sfsNode.Length) return ErrorNumber.NoError;
// Adjust length to not read past end of file
long bytesToRead = length;
if(sfsNode.Offset + bytesToRead > sfsNode.Length) bytesToRead = sfsNode.Length - sfsNode.Offset;
if(bytesToRead == 0) return ErrorNumber.NoError;
// Seek to the correct extent if needed
ErrorNumber errno = SeekToOffset(sfsNode);
if(errno != ErrorNumber.NoError) return errno;
// Read data from extent chain
long bufferOffset = 0;
while(bytesToRead > 0 && sfsNode.CurrentExtent != 0)
{
// Find the current extent B-node
errno = FindExtentBNode(sfsNode.CurrentExtent,
out uint extentKey,
out uint extentNext,
out uint extentBlocks);
if(errno != ErrorNumber.NoError) return errno;
// Calculate how many bytes are available in this extent
long extentSize = (long)extentBlocks << _blockShift;
long bytesInExtent = extentSize - sfsNode.ExtentOffset;
long bytesFromExtent = Math.Min(bytesToRead, bytesInExtent);
// Calculate starting block and offset within the extent
long offsetInExtent = sfsNode.ExtentOffset;
uint startBlock = extentKey + (uint)(offsetInExtent >> _blockShift);
var offsetInBlock = (int)(offsetInExtent & _blockSize - 1);
// Read blocks
while(bytesFromExtent > 0)
{
errno = ReadBlock(startBlock, out byte[] blockData);
if(errno != ErrorNumber.NoError) return errno;
var bytesFromBlock = (int)Math.Min(bytesFromExtent, _blockSize - offsetInBlock);
Array.Copy(blockData, offsetInBlock, buffer, bufferOffset, bytesFromBlock);
bufferOffset += bytesFromBlock;
bytesFromExtent -= bytesFromBlock;
bytesToRead -= bytesFromBlock;
read += bytesFromBlock;
sfsNode.Offset += bytesFromBlock;
sfsNode.ExtentOffset += bytesFromBlock;
offsetInBlock = 0;
startBlock++;
}
// Move to next extent if we've exhausted this one
if(sfsNode.ExtentOffset >= extentSize && extentNext != 0)
{
sfsNode.CurrentExtent = extentNext;
sfsNode.ExtentOffset = 0;
}
}
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber Stat(string path, out FileEntryInfo stat)
{
stat = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize the path
string normalizedPath = path ?? "/";
if(normalizedPath == "" || normalizedPath == ".") normalizedPath = "/";
// Root directory handling
if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
{
// Read root object to get its metadata
ErrorNumber errno = FindObjectNode(ROOTNODE, out uint rootBlock);
if(errno != ErrorNumber.NoError) return errno;
errno = ReadBlock(rootBlock, out byte[] rootData);
if(errno != ErrorNumber.NoError) return errno;
errno = FindObjectInContainer(rootData, ROOTNODE, out int rootOffset);
if(errno != ErrorNumber.NoError) return errno;
return StatFromObjectData(rootData, rootOffset, ROOTNODE, out stat);
}
// 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;
// Start traversal from root directory cache
Dictionary<string, uint> currentDirectory = _rootDirectoryCache;
uint targetNode = 0;
// Traverse all path components
for(var i = 0; i < pathComponents.Length; i++)
{
string component = pathComponents[i];
// Find the component in current directory (handle case sensitivity)
string foundKey = null;
foreach(string key in currentDirectory.Keys)
{
if(string.Equals(key,
component,
_caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
{
foundKey = key;
break;
}
}
if(foundKey == null) return ErrorNumber.NoSuchFile;
targetNode = currentDirectory[foundKey];
// If this is the last component, we found our target
if(i == pathComponents.Length - 1) break;
// Not the last component - read directory contents for next iteration
ErrorNumber errno = ReadDirectoryContents(targetNode, out Dictionary<string, uint> childEntries);
if(errno != ErrorNumber.NoError) return errno;
currentDirectory = childEntries;
}
// Read the target object's metadata
ErrorNumber findErr = FindObjectNode(targetNode, out uint objectBlock);
if(findErr != ErrorNumber.NoError) return findErr;
findErr = ReadBlock(objectBlock, out byte[] objectData);
if(findErr != ErrorNumber.NoError) return findErr;
findErr = FindObjectInContainer(objectData, targetNode, out int objectOffset);
if(findErr != ErrorNumber.NoError) return findErr;
return StatFromObjectData(objectData, objectOffset, targetNode, out stat);
}
/// <summary>Seeks to the correct extent for the current file offset</summary>
/// <param name="node">The file node</param>
/// <returns>Error code indicating success or failure</returns>
ErrorNumber SeekToOffset(SfsFileNode node)
{
// If we're before the current extent position, restart from the beginning
long currentExtentStart = node.Offset - node.ExtentOffset;
if(node.Offset < currentExtentStart || node.CurrentExtent == 0)
{
node.CurrentExtent = node.FirstExtent;
node.ExtentOffset = 0;
currentExtentStart = 0;
}
// Walk the extent chain to find the correct extent
while(node.CurrentExtent != 0)
{
ErrorNumber errno = FindExtentBNode(node.CurrentExtent, out _, out uint extentNext, out uint extentBlocks);
if(errno != ErrorNumber.NoError) return errno;
long extentSize = (long)extentBlocks << _blockShift;
long extentEnd = currentExtentStart + extentSize;
if(node.Offset < extentEnd)
{
// Found the correct extent
node.ExtentOffset = node.Offset - currentExtentStart;
return ErrorNumber.NoError;
}
// Move to next extent
currentExtentStart = extentEnd;
node.CurrentExtent = extentNext;
node.ExtentOffset = 0;
}
return ErrorNumber.InvalidArgument;
}
/// <summary>Finds an extent B-node by key in the extent B-tree</summary>
/// <param name="key">The extent key (block number)</param>
/// <param name="extentKey">Output: the extent's data block start</param>
/// <param name="extentNext">Output: the next extent in the chain</param>
/// <param name="extentBlocks">Output: number of blocks in this extent</param>
/// <returns>Error code indicating success or failure</returns>
ErrorNumber FindExtentBNode(uint key, out uint extentKey, out uint extentNext, out uint extentBlocks)
{
extentKey = 0;
extentNext = 0;
extentBlocks = 0;
// Start at the extent B-tree root
uint currentBlock = _rootBlock.extentbnoderoot;
while(true)
{
ErrorNumber errno = ReadBlock(currentBlock, out byte[] blockData);
if(errno != ErrorNumber.NoError) return errno;
// Validate block ID
var blockId = BigEndianBitConverter.ToUInt32(blockData, 0);
if(blockId != BNODECONTAINER_ID) return ErrorNumber.InvalidArgument;
// BTreeContainer starts at offset 12 (after header)
// nodecount (2) + isleaf (1) + nodesize (1)
var nodeCount = BigEndianBitConverter.ToUInt16(blockData, 12);
byte isLeaf = blockData[14];
byte nodeSize = blockData[15];
if(nodeCount == 0) return ErrorNumber.InvalidArgument;
// Search for the key in this container
// BNode entries start at offset 16
var nodeOffset = 16;
if(isLeaf != 0)
{
// Leaf node - contains ExtentBNode structures
// SFS\0: key (4) + next (4) + prev (4) + blocks (2) = 14 bytes
// SFS\2: key (4) + next (4) + prev (4) + blocks (4) = 16 bytes
// nodeSize field tells us the actual size
for(var i = 0; i < nodeCount; i++)
{
var nodeKey = BigEndianBitConverter.ToUInt32(blockData, nodeOffset);
if(nodeKey == key)
{
extentKey = nodeKey;
extentNext = BigEndianBitConverter.ToUInt32(blockData, nodeOffset + 4);
// Read blocks field - 16-bit in SFS\0 (nodeSize=14), 32-bit in SFS\2 (nodeSize=16)
extentBlocks = nodeSize >= 16
? BigEndianBitConverter.ToUInt32(blockData, nodeOffset + 12)
: BigEndianBitConverter.ToUInt16(blockData, nodeOffset + 12);
return ErrorNumber.NoError;
}
nodeOffset += nodeSize;
}
// Key not found
return ErrorNumber.InvalidArgument;
}
// Index node - find the child to descend into
// BNode: key (4) + data (4) = 8 bytes
uint childBlock = 0;
for(int i = nodeCount - 1; i >= 0; i--)
{
int entryOffset = nodeOffset + i * nodeSize;
var nodeKey = BigEndianBitConverter.ToUInt32(blockData, entryOffset);
if(key >= nodeKey)
{
childBlock = BigEndianBitConverter.ToUInt32(blockData, entryOffset + 4);
break;
}
}
if(childBlock == 0)
{
// Use first entry
childBlock = BigEndianBitConverter.ToUInt32(blockData, nodeOffset + 4);
}
currentBlock = childBlock;
}
}
/// <summary>Creates a FileEntryInfo from raw object data in an ObjectContainer</summary>
/// <param name="objectData">The ObjectContainer block data</param>
/// <param name="objectOffset">Offset to the object within the container</param>
/// <param name="objectNode">The object's node number</param>
/// <param name="stat">Output file entry information</param>
/// <returns>Error code indicating success or failure</returns>
ErrorNumber StatFromObjectData(byte[] objectData, int objectOffset, uint objectNode, out FileEntryInfo stat)
{
stat = null;
// Object structure (from objects.h):
// SFS\0 (25 bytes):
// owneruid (2) + ownergid (2) + objectnode (4) + protection (4) +
// data/hashtable (4) + size/firstdirblock (4) + datemodified (4) + bits (1)
// SFS\2 (27 bytes):
// owneruid (2) + ownergid (2) + objectnode (4) + protection (4) +
// data/hashtable (4) + size/firstdirblock (4) + sizeh (2) + datemodified (4) + bits (1)
if(objectOffset + _objectSize > objectData.Length) return ErrorNumber.InvalidArgument;
var ownerUid = BigEndianBitConverter.ToUInt16(objectData, objectOffset);
var ownerGid = BigEndianBitConverter.ToUInt16(objectData, objectOffset + 2);
var protection = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 8);
var dataOrHash = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 12);
var sizeOrDir = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 16);
ushort sizeh;
uint dateModified;
ObjectBits bits;
if(_isSfs2)
{
// SFS\2: sizeh at offset 20, datemodified at 22, bits at 26
sizeh = BigEndianBitConverter.ToUInt16(objectData, objectOffset + 20);
dateModified = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 22);
bits = (ObjectBits)objectData[objectOffset + 26];
}
else
{
// SFS\0: datemodified at offset 20, bits at 24
sizeh = 0;
dateModified = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 20);
bits = (ObjectBits)objectData[objectOffset + 24];
}
// Determine file attributes
FileAttributes attributes = FileAttributes.None;
if((bits & ObjectBits.Directory) != 0)
attributes |= FileAttributes.Directory;
else
attributes |= FileAttributes.File;
if((bits & ObjectBits.Hidden) != 0) attributes |= FileAttributes.Hidden;
if((bits & ObjectBits.Link) != 0 && (bits & ObjectBits.HardLink) == 0) attributes |= FileAttributes.Symlink;
// SFS protection bits: opposite of AmigaDOS
// Default is 0x0000000F (R, W, E, D set)
// If write bit (bit 1) is NOT set, file is read-only
if((protection & 0x02) == 0) attributes |= FileAttributes.ReadOnly;
// Calculate file size and blocks
long length = 0;
long blocks = 0;
if((bits & ObjectBits.Directory) == 0)
{
// In SFS\2, file size is 48-bit: (sizeOrDir << 16) | sizeh
// sizeh contains the LOW 16 bits, sizeOrDir contains the HIGH 32 bits
length = _isSfs2 ? (long)sizeOrDir << 16 | sizeh : sizeOrDir;
blocks = (length + _blockSize - 1) / _blockSize;
}
// Convert SFS timestamp (seconds since 1-1-1978) to DateTime
DateTime? lastWriteTimeUtc = _amigaEpoch.AddSeconds(dateModified);
stat = new FileEntryInfo
{
Attributes = attributes,
Inode = objectNode,
Length = length,
Blocks = blocks,
BlockSize = _blockSize,
Links = 1, // SFS doesn't track hard link count in the object
UID = ownerUid,
GID = ownerGid,
Mode = protection,
LastWriteTimeUtc = lastWriteTimeUtc
};
return ErrorNumber.NoError;
}
}