mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
537 lines
18 KiB
C#
537 lines
18 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : File.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Apple ProDOS 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 Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Structs;
|
|
using Aaru.Logging;
|
|
using Marshal = Aaru.Helpers.Marshal;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
/// <inheritdoc />
|
|
public sealed partial class ProDOSPlugin
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber OpenFile(string path, out IFileNode node)
|
|
{
|
|
node = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile: path='{0}'", path);
|
|
|
|
// Get the entry for this path
|
|
ErrorNumber errno = GetEntryForPath(path, out CachedEntry entry);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error getting entry for path: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Cannot open directories
|
|
if(entry.IsDirectory)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Cannot open directory as file");
|
|
|
|
return ErrorNumber.IsDirectory;
|
|
}
|
|
|
|
// Get file length - for extended files, we need to read the extended key block
|
|
long fileLength;
|
|
byte storageType = entry.StorageType;
|
|
ushort keyBlock = entry.KeyBlock;
|
|
|
|
if(storageType == EXTENDED_FILE_TYPE)
|
|
{
|
|
errno = ReadBlock(entry.KeyBlock, out byte[] extBlock);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading extended key block: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
ExtendedKeyBlock extKeyBlock = Marshal.ByteArrayToStructureLittleEndian<ExtendedKeyBlock>(extBlock);
|
|
|
|
// Data fork size
|
|
fileLength = extKeyBlock.data_fork.eof[0] |
|
|
extKeyBlock.data_fork.eof[1] << 8 |
|
|
extKeyBlock.data_fork.eof[2] << 16;
|
|
|
|
// Use data fork's storage type and key block
|
|
storageType = (byte)(extKeyBlock.data_fork.storage_type >> 4);
|
|
keyBlock = extKeyBlock.data_fork.key_block;
|
|
}
|
|
else
|
|
fileLength = entry.Eof;
|
|
|
|
// Create file node
|
|
var fileNode = new ProDosFileNode
|
|
{
|
|
Path = path,
|
|
Entry = entry,
|
|
Length = fileLength,
|
|
Offset = 0,
|
|
|
|
// Store effective storage type and key block (possibly from extended file's data fork)
|
|
EffectiveStorageType = storageType,
|
|
EffectiveKeyBlock = keyBlock
|
|
};
|
|
|
|
// Pre-load index block(s) for sapling and tree files
|
|
errno = LoadIndexBlocks(fileNode);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error loading index blocks: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
node = fileNode;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile successful: path='{0}', size={1}", path, fileLength);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseFile(IFileNode node)
|
|
{
|
|
if(node is not ProDosFileNode proDosNode) return ErrorNumber.InvalidArgument;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "CloseFile: path='{0}'", proDosNode.Path);
|
|
|
|
// Clear cached data
|
|
proDosNode.IndexBlock = null;
|
|
proDosNode.MasterIndexBlock = null;
|
|
proDosNode.CachedBlockData = null;
|
|
proDosNode.CachedBlockIndex = -1;
|
|
proDosNode.Offset = -1;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read)
|
|
{
|
|
read = 0;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
if(node is not ProDosFileNode proDosNode) return ErrorNumber.InvalidArgument;
|
|
|
|
if(proDosNode.Offset < 0) return ErrorNumber.InvalidArgument;
|
|
|
|
if(buffer == null || buffer.Length < length) return ErrorNumber.InvalidArgument;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadFile: path='{0}', offset={1}, length={2}",
|
|
proDosNode.Path,
|
|
proDosNode.Offset,
|
|
length);
|
|
|
|
// Check if at or past EOF
|
|
if(proDosNode.Offset >= proDosNode.Length)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile: at EOF");
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Adjust length if it would read past EOF
|
|
long bytesToRead = length;
|
|
|
|
if(proDosNode.Offset + bytesToRead > proDosNode.Length) bytesToRead = proDosNode.Length - proDosNode.Offset;
|
|
|
|
if(bytesToRead <= 0) return ErrorNumber.NoError;
|
|
|
|
// Read data block by block
|
|
long bufferOffset = 0;
|
|
|
|
while(bytesToRead > 0)
|
|
{
|
|
// Calculate which block we need
|
|
var blockIndex = (int)(proDosNode.Offset / 512);
|
|
var offsetInBlock = (int)(proDosNode.Offset % 512);
|
|
|
|
// Get the data block
|
|
ErrorNumber errno = ReadFileBlock(proDosNode, blockIndex, out byte[] blockData);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading file block {0}: {1}", blockIndex, errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Calculate how much to copy from this block
|
|
var bytesFromBlock = (int)Math.Min(bytesToRead, 512 - offsetInBlock);
|
|
|
|
// Copy to output buffer
|
|
Array.Copy(blockData, offsetInBlock, buffer, bufferOffset, bytesFromBlock);
|
|
|
|
bufferOffset += bytesFromBlock;
|
|
proDosNode.Offset += bytesFromBlock;
|
|
bytesToRead -= bytesFromBlock;
|
|
read += bytesFromBlock;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile successful: read {0} bytes, new offset={1}", read, proDosNode.Offset);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber Stat(string path, out FileEntryInfo stat)
|
|
{
|
|
stat = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Handle root directory specially
|
|
if(string.IsNullOrEmpty(path) || path == "/" || path == ".")
|
|
{
|
|
stat = new FileEntryInfo
|
|
{
|
|
Attributes = FileAttributes.Directory,
|
|
BlockSize = 512,
|
|
Blocks = 4, // Volume directory is always 4 blocks
|
|
CreationTime = _creationTime,
|
|
Inode = 2, // Root directory starts at block 2
|
|
Links = 1,
|
|
Mode = 0x16D, // drwxrw-r-x
|
|
DeviceNo = 0,
|
|
GID = 0,
|
|
UID = 0,
|
|
Length = 4 * 512
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Get the entry for this path
|
|
ErrorNumber errno = GetEntryForPath(path, out CachedEntry entry);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
stat = new FileEntryInfo
|
|
{
|
|
BlockSize = 512,
|
|
CreationTime = entry.CreationTime,
|
|
LastWriteTime = entry.ModificationTime,
|
|
Links = 1,
|
|
DeviceNo = 0,
|
|
GID = 0,
|
|
UID = 0
|
|
};
|
|
|
|
// Directory
|
|
if(entry.IsDirectory)
|
|
{
|
|
stat.Attributes = FileAttributes.Directory;
|
|
stat.Blocks = entry.BlocksUsed;
|
|
stat.Inode = entry.KeyBlock;
|
|
stat.Length = entry.BlocksUsed * 512;
|
|
stat.Mode = 0x16D; // drwxrw-r-x
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// File
|
|
stat.Inode = entry.KeyBlock;
|
|
|
|
// Set attributes
|
|
stat.Attributes = FileAttributes.File;
|
|
|
|
if((entry.Access & READ_ATTRIBUTE) == 0) stat.Attributes |= FileAttributes.Hidden;
|
|
|
|
if((entry.Access & WRITE_ATTRIBUTE) == 0) stat.Attributes |= FileAttributes.ReadOnly;
|
|
|
|
if((entry.Access & BACKUP_ATTRIBUTE) != 0) stat.Attributes |= FileAttributes.Archive;
|
|
|
|
// Calculate mode from access flags
|
|
uint mode = 0x8000; // Regular file
|
|
|
|
if((entry.Access & READ_ATTRIBUTE) != 0) mode |= 0x124; // r--r--r--
|
|
|
|
if((entry.Access & WRITE_ATTRIBUTE) != 0) mode |= 0x92; // -w--w--w-
|
|
|
|
stat.Mode = mode;
|
|
|
|
// For extended files (with resource fork), read the extended key block to get data fork size
|
|
if(entry.StorageType == EXTENDED_FILE_TYPE)
|
|
{
|
|
errno = ReadBlock(entry.KeyBlock, out byte[] extBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
ExtendedKeyBlock extKeyBlock = Marshal.ByteArrayToStructureLittleEndian<ExtendedKeyBlock>(extBlock);
|
|
|
|
// Data fork size (per user requirement: data fork mandates file size)
|
|
var dataForkEof = (uint)(extKeyBlock.data_fork.eof[0] |
|
|
extKeyBlock.data_fork.eof[1] << 8 |
|
|
extKeyBlock.data_fork.eof[2] << 16);
|
|
|
|
stat.Length = dataForkEof;
|
|
stat.Blocks = extKeyBlock.data_fork.blocks_used;
|
|
}
|
|
else
|
|
{
|
|
// Non-extended file: use entry's EOF and blocks_used
|
|
stat.Length = entry.Eof;
|
|
stat.Blocks = entry.BlocksUsed;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Loads index blocks for sapling and tree storage types</summary>
|
|
ErrorNumber LoadIndexBlocks(ProDosFileNode fileNode)
|
|
{
|
|
byte storageType = fileNode.EffectiveStorageType;
|
|
ushort keyBlock = fileNode.EffectiveKeyBlock;
|
|
|
|
switch(storageType)
|
|
{
|
|
case SEEDLING_FILE_TYPE:
|
|
// Seedling: key block is the single data block, no index needed
|
|
return ErrorNumber.NoError;
|
|
|
|
case SAPLING_FILE_TYPE:
|
|
{
|
|
// Sapling: key block is an indirect block containing up to 256 block pointers
|
|
ErrorNumber errno = ReadBlock(keyBlock, out byte[] indexBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Parse the indirect block (LSB in first 256 bytes, MSB in second 256 bytes)
|
|
fileNode.IndexBlock = new ushort[256];
|
|
|
|
for(var i = 0; i < 256; i++)
|
|
fileNode.IndexBlock[i] = (ushort)(indexBlock[i] | indexBlock[256 + i] << 8);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
case TREE_FILE_TYPE:
|
|
{
|
|
// Tree: key block is a master (double-indirect) block
|
|
ErrorNumber errno = ReadBlock(keyBlock, out byte[] masterBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Parse the master index block
|
|
fileNode.MasterIndexBlock = new ushort[256];
|
|
|
|
for(var i = 0; i < 256; i++)
|
|
fileNode.MasterIndexBlock[i] = (ushort)(masterBlock[i] | masterBlock[256 + i] << 8);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
default:
|
|
AaruLogging.Debug(MODULE_NAME, "Unknown storage type: 0x{0:X2}", storageType);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
}
|
|
|
|
/// <summary>Reads a specific data block from a file</summary>
|
|
ErrorNumber ReadFileBlock(ProDosFileNode fileNode, int blockIndex, out byte[] blockData)
|
|
{
|
|
blockData = null;
|
|
|
|
// Check if this block is already cached
|
|
if(fileNode.CachedBlockIndex == blockIndex && fileNode.CachedBlockData != null)
|
|
{
|
|
blockData = fileNode.CachedBlockData;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
byte storageType = fileNode.EffectiveStorageType;
|
|
|
|
ushort diskBlock;
|
|
|
|
switch(storageType)
|
|
{
|
|
case SEEDLING_FILE_TYPE:
|
|
// Seedling: key block is the single data block
|
|
if(blockIndex != 0) return ErrorNumber.InvalidArgument;
|
|
|
|
diskBlock = fileNode.EffectiveKeyBlock;
|
|
|
|
break;
|
|
|
|
case SAPLING_FILE_TYPE:
|
|
// Sapling: index block contains block pointers
|
|
if(blockIndex >= 256) return ErrorNumber.InvalidArgument;
|
|
|
|
diskBlock = fileNode.IndexBlock[blockIndex];
|
|
|
|
break;
|
|
|
|
case TREE_FILE_TYPE:
|
|
{
|
|
// Tree: master index -> index block -> data block
|
|
int masterIndex = blockIndex / 256;
|
|
int indexOffset = blockIndex % 256;
|
|
|
|
if(masterIndex >= 256) return ErrorNumber.InvalidArgument;
|
|
|
|
ushort indexBlockPtr = fileNode.MasterIndexBlock[masterIndex];
|
|
|
|
// Sparse block (index block pointer is 0)
|
|
if(indexBlockPtr == 0)
|
|
{
|
|
blockData = new byte[512];
|
|
fileNode.CachedBlockIndex = blockIndex;
|
|
fileNode.CachedBlockData = blockData;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Load the index block (cache it for subsequent reads in the same range)
|
|
if(fileNode.IndexBlock == null || fileNode.CachedIndexBlockNumber != masterIndex)
|
|
{
|
|
ErrorNumber errno = ReadBlock(indexBlockPtr, out byte[] indexBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
fileNode.IndexBlock = new ushort[256];
|
|
|
|
for(var i = 0; i < 256; i++)
|
|
fileNode.IndexBlock[i] = (ushort)(indexBlock[i] | indexBlock[256 + i] << 8);
|
|
|
|
fileNode.CachedIndexBlockNumber = masterIndex;
|
|
}
|
|
|
|
diskBlock = fileNode.IndexBlock[indexOffset];
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Sparse block (block pointer is 0 means unallocated = zeros)
|
|
if(diskBlock == 0)
|
|
{
|
|
blockData = new byte[512];
|
|
fileNode.CachedBlockIndex = blockIndex;
|
|
fileNode.CachedBlockData = blockData;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Read the actual data block
|
|
ErrorNumber readErrno = ReadBlock(diskBlock, out blockData);
|
|
|
|
if(readErrno != ErrorNumber.NoError) return readErrno;
|
|
|
|
// Cache the block
|
|
fileNode.CachedBlockIndex = blockIndex;
|
|
fileNode.CachedBlockData = blockData;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Gets a cached entry for the given path</summary>
|
|
/// <param name="path">Path to the file or directory</param>
|
|
/// <param name="entry">Output cached entry</param>
|
|
/// <returns>Error number</returns>
|
|
ErrorNumber GetEntryForPath(string path, out CachedEntry entry)
|
|
{
|
|
entry = null;
|
|
|
|
if(string.IsNullOrEmpty(path) || path == "/" || path == ".") return ErrorNumber.IsDirectory;
|
|
|
|
string[] pathComponents = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if(pathComponents.Length == 0) return ErrorNumber.IsDirectory;
|
|
|
|
// Start from root directory cache
|
|
Dictionary<string, CachedEntry> currentDir = _rootDirectoryCache;
|
|
|
|
for(var i = 0; i < pathComponents.Length; i++)
|
|
{
|
|
string component = pathComponents[i];
|
|
|
|
if(component is "." or "..") continue;
|
|
|
|
if(!currentDir.TryGetValue(component, out CachedEntry currentEntry)) return ErrorNumber.NoSuchFile;
|
|
|
|
// Last component - return this entry
|
|
if(i == pathComponents.Length - 1)
|
|
{
|
|
entry = currentEntry;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Intermediate component must be a directory
|
|
if(!currentEntry.IsDirectory) return ErrorNumber.NotDirectory;
|
|
|
|
// Read subdirectory contents
|
|
ErrorNumber errno =
|
|
ReadDirectoryContents(currentEntry.KeyBlock, false, out Dictionary<string, CachedEntry> subDir);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
currentDir = subDir;
|
|
}
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
/// <summary>Applies GS/OS case bits to a filename</summary>
|
|
static string ApplyCaseBits(string name, ushort caseBits)
|
|
{
|
|
if((caseBits & 0x8000) == 0) return name;
|
|
|
|
char[] chars = name.ToCharArray();
|
|
var bit = 0x4000;
|
|
|
|
for(var i = 0; i < chars.Length && bit > 0; i++)
|
|
{
|
|
if((caseBits & bit) != 0) chars[i] = char.ToLower(chars[i]);
|
|
|
|
bit >>= 1;
|
|
}
|
|
|
|
return new string(chars);
|
|
}
|
|
} |