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

410 lines
16 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : File.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : RT-11 file system plugin.
//
// --[ Description ] ----------------------------------------------------------
//
// Identifies the RT-11 file system and shows information.
//
// --[ 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.Runtime.InteropServices;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
namespace Aaru.Filesystems;
// Information from http://www.trailing-edge.com/~shoppa/rt11fs/
/// <inheritdoc />
public sealed partial class RT11
{
/// <inheritdoc />
public ErrorNumber OpenFile(string path, out IFileNode node)
{
node = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize the path - RT-11 is a flat filesystem
string filename = path;
if(string.IsNullOrEmpty(filename) || filename == "/" || filename == ".") return ErrorNumber.IsDirectory;
// Remove leading slash if present
if(filename.StartsWith("/", StringComparison.Ordinal)) filename = filename[1..];
// Check if the file exists in cache
if(!_rootDirectoryCache.ContainsKey(filename)) return ErrorNumber.NoSuchFile;
// Get file information from directory
ErrorNumber error = GetFileStartBlockAndLength(filename, out uint startBlock, out uint lengthInBlocks);
if(error != ErrorNumber.NoError) return error;
// Create file node
node = new RT11FileNode
{
Path = path,
Length = lengthInBlocks * BLOCK_SIZE_BYTES,
Offset = 0,
StartBlock = startBlock,
LengthInBlocks = lengthInBlocks
};
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber CloseFile(IFileNode node)
{
if(!_mounted) return ErrorNumber.AccessDenied;
if(node is not RT11FileNode) 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 RT11FileNode rt11Node) return ErrorNumber.InvalidArgument;
// Can't read past end of file
if(rt11Node.Offset >= rt11Node.Length) return ErrorNumber.NoError;
// Limit read to remaining file size
if(length > rt11Node.Length - rt11Node.Offset) length = rt11Node.Length - rt11Node.Offset;
// Calculate which block to start reading from
long offsetInFile = rt11Node.Offset;
ulong currentBlock = rt11Node.StartBlock + (ulong)(offsetInFile / BLOCK_SIZE_BYTES);
var offsetInBlock = (int)(offsetInFile % BLOCK_SIZE_BYTES);
long bytesRemaining = length;
var bufferOffset = 0;
// RT-11 files are stored in contiguous blocks, so we can read them sequentially
while(bytesRemaining > 0)
{
// Read the current block
ErrorNumber errno =
_imagePlugin.ReadSector(_partition.Start + currentBlock, false, out byte[] blockData, out _);
if(errno != ErrorNumber.NoError) return errno;
// Calculate how much to read from this block
var bytesToRead = (int)Math.Min(bytesRemaining, BLOCK_SIZE_BYTES - offsetInBlock);
// Copy data from block to buffer
Array.Copy(blockData, offsetInBlock, buffer, bufferOffset, bytesToRead);
bufferOffset += bytesToRead;
bytesRemaining -= bytesToRead;
rt11Node.Offset += bytesToRead;
currentBlock++;
offsetInBlock = 0; // After first block, always start at beginning
}
read = bufferOffset;
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
if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
{
stat = new FileEntryInfo
{
Attributes = FileAttributes.Directory,
Inode = _firstDirectoryBlock,
Links = 1,
BlockSize = BLOCK_SIZE_BYTES
};
return ErrorNumber.NoError;
}
// RT-11 only has a root directory (flat filesystem)
// Remove leading slash if present
string filename = normalizedPath.StartsWith("/", StringComparison.Ordinal)
? normalizedPath[1..]
: normalizedPath;
// Check if the file exists in cache
if(!_rootDirectoryCache.ContainsKey(filename)) return ErrorNumber.NoSuchFile;
// Get file information from directory entry
ErrorNumber error = GetFileInfo(filename, out stat);
return error;
}
/// <summary>Gets the starting block and length for a file</summary>
/// <param name="filename">Filename</param>
/// <param name="startBlock">Output starting block number</param>
/// <param name="lengthInBlocks">Output file length in blocks</param>
/// <returns>Error code</returns>
ErrorNumber GetFileStartBlockAndLength(string filename, out uint startBlock, out uint lengthInBlocks)
{
startBlock = 0;
lengthInBlocks = 0;
// Read the first directory segment to get file information
ErrorNumber errno = _imagePlugin.ReadSectors(_partition.Start + _firstDirectoryBlock,
false,
2,
out byte[] dirSegmentData,
out _);
if(errno != ErrorNumber.NoError) return errno;
// Parse directory segment header
DirectorySegmentHeader segmentHeader =
Marshal.PtrToStructure<DirectorySegmentHeader>(Marshal.UnsafeAddrOfPinnedArrayElement(dirSegmentData, 0));
// Directory entries start after the 5-word header (10 bytes)
// The starting block for files is in word 5 of the header (dataBlockStart)
var offset = 10;
int entrySize = DIRECTORY_ENTRY_WORDS * 2 + segmentHeader.extraBytesPerEntry;
var currentBlockNum = (uint)segmentHeader.dataBlockStart;
while(offset + entrySize <= dirSegmentData.Length)
{
DirectoryEntry entry =
Marshal.PtrToStructure<DirectoryEntry>(Marshal.UnsafeAddrOfPinnedArrayElement(dirSegmentData, offset));
// Check for end-of-segment marker
if((entry.status & 0xFF00) == E_EOS) break;
// Process permanent files
if((entry.status & 0xFF00) == E_PERM)
{
string entryFilename = DecodeRadix50Filename(entry.filename1, entry.filename2, entry.filetype);
if(string.Equals(entryFilename, filename, StringComparison.OrdinalIgnoreCase))
{
startBlock = currentBlockNum;
lengthInBlocks = entry.length;
return ErrorNumber.NoError;
}
currentBlockNum += entry.length;
}
else if((entry.status & 0xFF00) == E_TENT || (entry.status & 0xFF00) == E_MPTY)
{
// Skip tentative and empty entries, but advance block counter
currentBlockNum += entry.length;
}
offset += entrySize;
}
return ErrorNumber.NoSuchFile;
}
/// <summary>Gets file information from directory entry</summary>
/// <param name="filename">Filename</param>
/// <param name="stat">Output file entry info</param>
/// <returns>Error code</returns>
ErrorNumber GetFileInfo(string filename, out FileEntryInfo stat)
{
stat = null;
// Read the first directory segment to get file information
ErrorNumber errno = _imagePlugin.ReadSectors(_partition.Start + _firstDirectoryBlock,
false,
2,
out byte[] dirSegmentData,
out _);
if(errno != ErrorNumber.NoError) return errno;
// Parse directory segment header
DirectorySegmentHeader segmentHeader =
Marshal.PtrToStructure<DirectorySegmentHeader>(Marshal.UnsafeAddrOfPinnedArrayElement(dirSegmentData, 0));
// Directory entries start after the 5-word header (10 bytes)
var offset = 10;
int entrySize = DIRECTORY_ENTRY_WORDS * 2 + segmentHeader.extraBytesPerEntry;
var currentBlockNum = (uint)segmentHeader.dataBlockStart;
while(offset + entrySize <= dirSegmentData.Length)
{
DirectoryEntry entry =
Marshal.PtrToStructure<DirectoryEntry>(Marshal.UnsafeAddrOfPinnedArrayElement(dirSegmentData, offset));
// Check for end-of-segment marker
if((entry.status & 0xFF00) == E_EOS) break;
// Process permanent files only
if((entry.status & 0xFF00) == E_PERM)
{
string entryFilename = DecodeRadix50Filename(entry.filename1, entry.filename2, entry.filetype);
if(string.Equals(entryFilename, filename, StringComparison.OrdinalIgnoreCase))
{
// Build FileEntryInfo from directory entry
stat = new FileEntryInfo
{
Attributes = FileAttributes.File,
Inode = currentBlockNum,
Links = 1,
BlockSize = BLOCK_SIZE_BYTES,
Length = entry.length * BLOCK_SIZE_BYTES, // Convert blocks to bytes
Blocks = entry.length
};
// Decode creation date if present
if(entry.creationDate != 0) stat.CreationTimeUtc = DecodeRT11Date(entry.creationDate);
return ErrorNumber.NoError;
}
currentBlockNum += entry.length;
}
else if((entry.status & 0xFF00) == E_TENT || (entry.status & 0xFF00) == E_MPTY)
currentBlockNum += entry.length;
offset += entrySize;
}
return ErrorNumber.NoSuchFile;
}
/// <summary>Decodes RT-11 date format to DateTime</summary>
/// <param name="dateWord">RT-11 date word</param>
/// <returns>DateTime in UTC</returns>
static DateTime DecodeRT11Date(ushort dateWord)
{
// RT-11 date format:
// Bits 0-4: Day (1-31)
// Bits 5-8: Month (1-12)
// Bits 9-13: Year (0-31, representing years since base year)
// Bits 14-15: Age (0-3, each age represents 32 years)
int day = dateWord & DATE_DAY_MASK;
int month = (dateWord & DATE_MONTH_MASK) >> DATE_MONTH_SHIFT;
int year = (dateWord & DATE_YEAR_MASK) >> DATE_YEAR_SHIFT;
int age = (dateWord & DATE_AGE_MASK) >> DATE_AGE_SHIFT;
// Calculate actual year based on age
int baseYear = age switch
{
0 => BASE_YEAR_AGE0, // 1972
1 => BASE_YEAR_AGE1, // 2004
2 => BASE_YEAR_AGE2, // 2036
3 => BASE_YEAR_AGE3, // 2068
_ => BASE_YEAR_AGE0
};
int actualYear = baseYear + year;
// Validate date components
if(day < 1 || day > 31 || month < 1 || month > 12) return DateTime.UnixEpoch;
try
{
return new DateTime(actualYear, month, day, 0, 0, 0, DateTimeKind.Utc);
}
catch
{
return DateTime.UnixEpoch;
}
}
/// <summary>Gets the file length from cached directory information</summary>
/// <param name="filename">Filename</param>
/// <param name="fileLength">Output file length in blocks</param>
/// <returns>Error code</returns>
ErrorNumber GetFileLengthFromCache(string filename, out uint fileLength)
{
fileLength = 0;
if(!_rootDirectoryCache.ContainsKey(filename)) return ErrorNumber.NoSuchFile;
// Read the first directory segment to get file length
ErrorNumber errno = _imagePlugin.ReadSectors(_partition.Start + _firstDirectoryBlock,
false,
2,
out byte[] dirSegmentData,
out _);
if(errno != ErrorNumber.NoError) return errno;
// Parse directory segment header
DirectorySegmentHeader segmentHeader =
Marshal.PtrToStructure<DirectorySegmentHeader>(Marshal.UnsafeAddrOfPinnedArrayElement(dirSegmentData, 0));
// Directory entries start after the 5-word header (10 bytes)
var offset = 10;
int entrySize = DIRECTORY_ENTRY_WORDS * 2 + segmentHeader.extraBytesPerEntry;
while(offset + entrySize <= dirSegmentData.Length)
{
DirectoryEntry entry =
Marshal.PtrToStructure<DirectoryEntry>(Marshal.UnsafeAddrOfPinnedArrayElement(dirSegmentData, offset));
// Check for end-of-segment marker
if((entry.status & 0xFF00) == E_EOS) break;
// Process permanent files only
if((entry.status & 0xFF00) == E_PERM)
{
string entryFilename = DecodeRadix50Filename(entry.filename1, entry.filename2, entry.filetype);
if(string.Equals(entryFilename, filename, StringComparison.OrdinalIgnoreCase))
{
fileLength = entry.length;
return ErrorNumber.NoError;
}
}
offset += entrySize;
}
return ErrorNumber.NoSuchFile;
}
}