mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
458 lines
16 KiB
C#
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;
|
|
}
|
|
} |