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

458 lines
16 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : File.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Amiga Fast 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 Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class AmigaDOSPlugin
{
/// <inheritdoc />
public ErrorNumber ReadLink(string path, out string dest)
{
dest = null;
if(!_mounted) return ErrorNumber.AccessDenied;
AaruLogging.Debug(MODULE_NAME, "ReadLink: path='{0}'", path);
// Find the file block
ErrorNumber error = GetBlockForPath(path, out uint blockNum);
if(error != ErrorNumber.NoError) return error;
// Read the header block
error = ReadBlock(blockNum, out byte[] blockData);
if(error != ErrorNumber.NoError) return error;
// Validate block type (should be T_SHORT/T_HEADER = 2)
var type = BigEndianBitConverter.ToUInt32(blockData, 0x00);
if(type != TYPE_HEADER) return ErrorNumber.InvalidArgument;
// Get secondary type
int secTypeOffset = blockData.Length - 4;
var secType = BigEndianBitConverter.ToUInt32(blockData, secTypeOffset);
// Check if it's a soft link (ST_LSOFT = 3)
if(secType != 3) return ErrorNumber.InvalidArgument;
// Symbolic link target path is stored starting at BLK_SYMBOLICNAME_START (offset 6 in longs = 24 bytes)
// and ending at BLK_SYMBOLICNAME_END (SizeBlock - 51 in longs)
int sizeBlock = blockData.Length / 4;
int symbolicNameStart = 6 * 4; // BLK_SYMBOLICNAME_START = 6, convert to bytes
int symbolicNameEnd = (sizeBlock - 51) * 4; // BLK_SYMBOLICNAME_END = SizeBlock - 51
// Find the null terminator within the symbolic name area
var actualLength = 0;
for(int i = symbolicNameStart; i < symbolicNameEnd && blockData[i] != 0; i++) actualLength++;
// Extract the symbolic link target
dest = _encoding.GetString(blockData, symbolicNameStart, actualLength);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber OpenFile(string path, out IFileNode node)
{
node = null;
if(!_mounted) return ErrorNumber.AccessDenied;
AaruLogging.Debug(MODULE_NAME, "OpenFile: path='{0}'", path);
// Find the file block
ErrorNumber error = GetBlockForPath(path, out uint blockNum);
if(error != ErrorNumber.NoError) return error;
// Read the header block
error = ReadBlock(blockNum, out byte[] blockData);
if(error != ErrorNumber.NoError) return error;
// Validate block type (should be T_SHORT/T_HEADER = 2)
var type = BigEndianBitConverter.ToUInt32(blockData, 0x00);
if(type != TYPE_HEADER) return ErrorNumber.InvalidArgument;
// Get secondary type
int secTypeOffset = blockData.Length - 4;
var secType = BigEndianBitConverter.ToUInt32(blockData, secTypeOffset);
// Check if it's a file (ST_FILE = -3 or ST_LFILE = -4)
var signedSecType = (int)secType;
if(secType > 0x7FFFFFFF) signedSecType = (int)(secType - 0x100000000);
if(signedSecType != -3 && signedSecType != -4) return ErrorNumber.IsDirectory;
// Get file size from BLK_BYTE_SIZE (SizeBlock - 47)
int byteSizeOffset = blockData.Length - 47 * 4;
var fileSize = BigEndianBitConverter.ToUInt32(blockData, byteSizeOffset);
// Calculate the size of the data block pointer table
// SizeBlock in longs = blockData.Length / 4
int sizeBlock = blockData.Length / 4;
// BLK_TABLE_END = SizeBlock - 51
int tableEnd = sizeBlock - 51;
// Create file node
node = new AmigaDOSFileNode
{
Path = path,
Length = fileSize,
Offset = 0,
HeaderBlock = blockNum,
FileSize = fileSize,
CurrentExtensionBlock = blockNum,
CurrentFileKey = tableEnd, // Start at end of table (data blocks listed in reverse order)
CurrentByteInBlock = 0
};
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber CloseFile(IFileNode node)
{
if(!_mounted) return ErrorNumber.AccessDenied;
if(node is not AmigaDOSFileNode) 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 AmigaDOSFileNode myNode) return ErrorNumber.InvalidArgument;
// Can't read past end of file
if(myNode.Offset >= myNode.Length) return ErrorNumber.NoError;
// Limit read to remaining file size
if(length > myNode.Length - myNode.Offset) length = myNode.Length - myNode.Offset;
// Calculate block sizes based on filesystem type
var sizeBlock = (int)(_blockSize / 4); // Size in longs
int tableEnd = sizeBlock - 51; // BLK_TABLE_END
int extensionOffset = sizeBlock - 2; // BLK_EXTENSION
// Read the current extension block
ErrorNumber error = ReadBlock(myNode.CurrentExtensionBlock, out byte[] extensionData);
if(error != ErrorNumber.NoError) return error;
long bytesRemaining = length;
var bufferOffset = 0;
while(bytesRemaining > 0)
{
// Do we need to read the next extension block?
if(myNode.CurrentFileKey < BLK_TABLE_START)
{
// Read next extension block pointer from BLK_EXTENSION
var nextExtension = BigEndianBitConverter.ToUInt32(extensionData, extensionOffset * 4);
if(nextExtension == 0) break; // No more data
myNode.CurrentExtensionBlock = nextExtension;
myNode.CurrentFileKey = tableEnd; // Reset to end of table
error = ReadBlock(nextExtension, out extensionData);
if(error != ErrorNumber.NoError) return error;
}
// Get the data block pointer from the table
var dataBlockNum = BigEndianBitConverter.ToUInt32(extensionData, myNode.CurrentFileKey * 4);
if(dataBlockNum == 0) break; // No more data
// Read the data block
error = ReadBlock(dataBlockNum, out byte[] dataBlock);
if(error != ErrorNumber.NoError) return error;
// Calculate how much data is in this block and where it starts
int dataOffset;
int dataInBlock;
if(_isFfs)
{
// FFS: data starts at beginning of block
dataOffset = myNode.CurrentByteInBlock;
dataInBlock = (int)_blockSize - myNode.CurrentByteInBlock;
}
else
{
// OFS: data has a header, actual data size is in the header
var ofsDataSize = BigEndianBitConverter.ToUInt32(dataBlock, 12); // BLK_DATA_SIZE = 3
dataOffset = OFS_DATA_HEADER_SIZE + myNode.CurrentByteInBlock;
dataInBlock = (int)ofsDataSize - myNode.CurrentByteInBlock;
}
// Limit to what we actually need
if(dataInBlock > bytesRemaining) dataInBlock = (int)bytesRemaining;
// Copy data to buffer
Array.Copy(dataBlock, dataOffset, buffer, bufferOffset, dataInBlock);
bufferOffset += dataInBlock;
bytesRemaining -= dataInBlock;
myNode.Offset += dataInBlock;
// Update position within block
if(_isFfs)
{
if(myNode.CurrentByteInBlock + dataInBlock >= _blockSize)
{
myNode.CurrentByteInBlock = 0;
myNode.CurrentFileKey--;
}
else
myNode.CurrentByteInBlock += dataInBlock;
}
else
{
// For OFS, check against the actual data size in this block
var ofsDataSize = BigEndianBitConverter.ToUInt32(dataBlock, 12);
if(myNode.CurrentByteInBlock + dataInBlock >= ofsDataSize)
{
myNode.CurrentByteInBlock = 0;
myNode.CurrentFileKey--;
}
else
myNode.CurrentByteInBlock += dataInBlock;
}
}
read = bufferOffset;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber Stat(string path, out FileEntryInfo stat)
{
stat = null;
if(!_mounted) return ErrorNumber.AccessDenied;
AaruLogging.Debug(MODULE_NAME, "Stat: path='{0}'", path);
// Normalize the path
string normalizedPath = path ?? "/";
if(normalizedPath == "" || normalizedPath == ".") normalizedPath = "/";
// Root directory
if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
{
stat = new FileEntryInfo
{
Attributes = FileAttributes.Directory,
Inode = _rootBlockSector,
Links = 1,
BlockSize = _blockSize,
LastWriteTimeUtc = DateHandlers.AmigaToDateTime(_rootBlock.rDays, _rootBlock.rMins, _rootBlock.rTicks),
CreationTimeUtc = DateHandlers.AmigaToDateTime(_rootBlock.cDays, _rootBlock.cMins, _rootBlock.cTicks)
};
return ErrorNumber.NoError;
}
// Find the entry
ErrorNumber error = GetBlockForPath(normalizedPath, out uint blockNum);
if(error != ErrorNumber.NoError) return error;
// Read the block
error = ReadBlock(blockNum, out byte[] blockData);
if(error != ErrorNumber.NoError) return error;
// Validate block type
var type = BigEndianBitConverter.ToUInt32(blockData, 0x00);
if(type != TYPE_HEADER) return ErrorNumber.InvalidArgument;
// Get secondary type
int secTypeOffset = blockData.Length - 4;
var secType = BigEndianBitConverter.ToUInt32(blockData, secTypeOffset);
// Build stat from block data
stat = BuildStatFromBlock(blockData, blockNum, secType);
return ErrorNumber.NoError;
}
/// <summary>Builds a FileEntryInfo from block data</summary>
/// <param name="blockData">Block data</param>
/// <param name="blockNum">Block number</param>
/// <param name="secType">Secondary type</param>
/// <returns>FileEntryInfo structure</returns>
FileEntryInfo BuildStatFromBlock(byte[] blockData, uint blockNum, uint secType)
{
// Block offsets (in longs from end of block)
int protectOffset = blockData.Length - 48 * 4; // BLK_PROTECT = SizeBlock - 48
int byteSizeOffset = blockData.Length - 47 * 4; // BLK_BYTE_SIZE = SizeBlock - 47
int daysOffset = blockData.Length - 23 * 4; // BLK_DAYS = SizeBlock - 23
int minsOffset = blockData.Length - 22 * 4; // BLK_MINS = SizeBlock - 22
int ticksOffset = blockData.Length - 21 * 4; // BLK_TICKS = SizeBlock - 21
var protect = BigEndianBitConverter.ToUInt32(blockData, protectOffset);
var byteSize = BigEndianBitConverter.ToUInt32(blockData, byteSizeOffset);
var days = BigEndianBitConverter.ToUInt32(blockData, daysOffset);
var mins = BigEndianBitConverter.ToUInt32(blockData, minsOffset);
var ticks = BigEndianBitConverter.ToUInt32(blockData, ticksOffset);
var stat = new FileEntryInfo
{
Inode = blockNum,
Links = 1,
BlockSize = _blockSize,
LastWriteTimeUtc = DateHandlers.AmigaToDateTime(days, mins, ticks),
Mode = AmigaProtectToUnixMode(protect)
};
// Determine type from secondary type
// ST_FILE = -3, ST_ROOT = 1, ST_USERDIR = 2, ST_LINKDIR = 4, ST_LFILE = -4, ST_LSOFT = 3
var signedSecType = (int)secType;
// Handle signed comparison for negative values
if(secType > 0x7FFFFFFF) signedSecType = (int)(secType - 0x100000000);
switch(signedSecType)
{
case 1: // ST_ROOT
case 2: // ST_USERDIR
case 4: // ST_LINKDIR
stat.Attributes = FileAttributes.Directory;
break;
case -3: // ST_FILE
stat.Attributes = FileAttributes.File;
stat.Length = byteSize;
break;
case -4: // ST_LFILE (hard link to file)
stat.Attributes = FileAttributes.File;
stat.Length = byteSize;
break;
case 3: // ST_LSOFT (soft link)
stat.Attributes = FileAttributes.Symlink;
break;
default:
stat.Attributes = FileAttributes.File;
stat.Length = byteSize;
break;
}
// Apply Amiga protection flags to attributes
// Note: Amiga protection bits for owner are active-low (0 = allowed)
// FIBB_ARCHIVE = 4
if((protect & 1 << 4) != 0) stat.Attributes |= FileAttributes.Archive;
// FIBB_PURE = 5 - maps to System (resident)
if((protect & 1 << 5) != 0) stat.Attributes |= FileAttributes.System;
// FIBB_SCRIPT = 6
// No direct mapping, could use Hidden but that's not accurate
// Calculate blocks used (for files)
if(stat.Length > 0) stat.Blocks = (stat.Length + _blockSize - 1) / _blockSize;
return stat;
}
/// <summary>Converts Amiga protection bits to Unix-style mode</summary>
/// <param name="protect">Amiga protection bits</param>
/// <returns>Unix-style mode</returns>
static uint AmigaProtectToUnixMode(uint protect)
{
// Amiga owner bits are active-low (0 = allowed), Unix are active-high
// FIBB_READ = 3, FIBB_WRITE = 2, FIBB_EXECUTE = 1, FIBB_DELETE = 0
uint mode = 0;
// Owner permissions (active-low, so invert)
if((protect & 1 << 3) == 0) // FIBB_READ
mode |= 0x100; // S_IRUSR
if((protect & 1 << 2) == 0) // FIBB_WRITE
mode |= 0x080; // S_IWUSR
if((protect & 1 << 1) == 0) // FIBB_EXECUTE
mode |= 0x040; // S_IXUSR
// Group permissions (active-high for FIBB_GRP_*)
// FIBB_GRP_READ = 11, FIBB_GRP_WRITE = 10, FIBB_GRP_EXECUTE = 9
if((protect & 1 << 11) != 0) mode |= 0x020; // S_IRGRP
if((protect & 1 << 10) != 0) mode |= 0x010; // S_IWGRP
if((protect & 1 << 9) != 0) mode |= 0x008; // S_IXGRP
// Other permissions (active-high for FIBB_OTR_*)
// FIBB_OTR_READ = 15, FIBB_OTR_WRITE = 14, FIBB_OTR_EXECUTE = 13
if((protect & 1 << 15) != 0) mode |= 0x004; // S_IROTH
if((protect & 1 << 14) != 0) mode |= 0x002; // S_IWOTH
if((protect & 1 << 13) != 0) mode |= 0x001; // S_IXOTH
return mode;
}
}