mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
425 lines
15 KiB
C#
425 lines
15 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : File.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Atheos 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 System.Linq;
|
|
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 AtheOS
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadLink(string path, out string dest)
|
|
{
|
|
dest = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadLink: path='{0}'", path);
|
|
|
|
// Get the inode for the path
|
|
ErrorNumber errno = GetInodeForPath(path, out Inode inode, out byte[] _);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error getting inode for path: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Validate inode magic
|
|
if(inode.magic1 != INODE_MAGIC)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Invalid i-node magic: 0x{0:X8}", inode.magic1);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Check if it's a symbolic link (S_IFLNK = 0xA000)
|
|
int fileType = inode.mode & 0xF000;
|
|
|
|
if(fileType != 0xA000)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Path is not a symbolic link (mode=0x{0:X})", inode.mode);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Read the symlink target from the data stream
|
|
if(inode.data.size <= 0)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Symlink has empty data stream");
|
|
dest = "";
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Limit symlink size to reasonable value
|
|
if(inode.data.size > 4096)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Symlink data size too large: {0}", inode.data.size);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
errno = ReadFromDataStream(inode.data, 0, (int)inode.data.size, out byte[] linkData);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading symlink data: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Convert to string, stopping at null terminator if present
|
|
dest = _encoding.GetString(linkData).TrimEnd('\0');
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadLink successful: target='{0}'", dest);
|
|
|
|
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);
|
|
|
|
// Get the inode for the path
|
|
ErrorNumber errno = GetInodeForPath(path, out Inode inode, out byte[] _);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error getting inode for path: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Validate inode magic
|
|
if(inode.magic1 != INODE_MAGIC)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Invalid i-node magic: 0x{0:X8}", inode.magic1);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Check if it's a regular file
|
|
int fileType = inode.mode & 0xF000;
|
|
|
|
if(fileType == 0x4000) // S_IFDIR
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Cannot open directory as file");
|
|
|
|
return ErrorNumber.IsDirectory;
|
|
}
|
|
|
|
// Create file node
|
|
node = new AtheosFileNode
|
|
{
|
|
Path = path,
|
|
Inode = inode,
|
|
DataStream = inode.data,
|
|
Length = inode.data.size,
|
|
Offset = 0
|
|
};
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile successful: path='{0}', size={1}", path, inode.data.size);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseFile(IFileNode node)
|
|
{
|
|
if(node is not AtheosFileNode atheosNode) return ErrorNumber.InvalidArgument;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "CloseFile: path='{0}'", atheosNode.Path);
|
|
|
|
// Reset the node
|
|
atheosNode.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 AtheosFileNode atheosNode) return ErrorNumber.InvalidArgument;
|
|
|
|
if(atheosNode.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}",
|
|
atheosNode.Path,
|
|
atheosNode.Offset,
|
|
length);
|
|
|
|
// Check if we're at or past EOF
|
|
if(atheosNode.Offset >= atheosNode.Length)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile: at EOF");
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Adjust length if it would read past EOF
|
|
long bytesToRead = length;
|
|
|
|
if(atheosNode.Offset + bytesToRead > atheosNode.Length) bytesToRead = atheosNode.Length - atheosNode.Offset;
|
|
|
|
if(bytesToRead <= 0) return ErrorNumber.NoError;
|
|
|
|
// Read from the data stream
|
|
ErrorNumber errno = ReadFromDataStream(atheosNode.DataStream,
|
|
atheosNode.Offset,
|
|
(int)bytesToRead,
|
|
out byte[] readBuffer);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading from data stream: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Copy to output buffer
|
|
Array.Copy(readBuffer, 0, buffer, 0, bytesToRead);
|
|
read = bytesToRead;
|
|
atheosNode.Offset += bytesToRead;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadFile successful: read {0} bytes, new offset={1}", read, atheosNode.Offset);
|
|
|
|
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 and parse the path
|
|
string normalizedPath = path ?? "/";
|
|
|
|
if(normalizedPath == "" || normalizedPath == ".") normalizedPath = "/";
|
|
|
|
// Root directory handling
|
|
if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Calculate block address for root directory
|
|
long rootBlockAddress = (long)_superblock.root_dir_ag * _superblock.blocks_per_ag +
|
|
_superblock.root_dir_start;
|
|
|
|
// Read the actual root i-node to get its timestamps
|
|
ErrorNumber rootError = ReadInode(rootBlockAddress, out Inode rootInode);
|
|
|
|
if(rootError != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading root i-node: {0}", rootError);
|
|
|
|
return rootError;
|
|
}
|
|
|
|
// Return stats for root directory with real i-node data
|
|
// AtheOS timestamps are in microseconds since epoch
|
|
stat = new FileEntryInfo
|
|
{
|
|
Attributes = FileAttributes.Directory,
|
|
Inode = (ulong)rootBlockAddress,
|
|
CreationTimeUtc =
|
|
DateHandlers.UnixUnsignedToDateTime((ulong)(rootInode.create_time / 1000000))
|
|
.AddTicks(rootInode.create_time % 1000000 * 10),
|
|
LastWriteTimeUtc =
|
|
DateHandlers.UnixUnsignedToDateTime((ulong)(rootInode.modified_time / 1000000))
|
|
.AddTicks(rootInode.modified_time % 1000000 * 10),
|
|
AccessTimeUtc =
|
|
DateHandlers.UnixUnsignedToDateTime((ulong)(rootInode.modified_time / 1000000))
|
|
.AddTicks(rootInode.modified_time % 1000000 * 10),
|
|
UID = (ulong)rootInode.uid,
|
|
GID = (ulong)rootInode.gid,
|
|
Mode = (uint)rootInode.mode,
|
|
Links = (ulong)rootInode.link_count
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Remove leading slash and split path
|
|
string pathWithoutLeadingSlash = normalizedPath.StartsWith("/", StringComparison.Ordinal)
|
|
? normalizedPath[1..]
|
|
: normalizedPath;
|
|
|
|
string[] pathComponents = pathWithoutLeadingSlash.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
|
.Where(static c => c != "." && c != "..")
|
|
.ToArray();
|
|
|
|
if(pathComponents.Length == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
// Navigate to the target file/directory
|
|
Dictionary<string, long> currentEntries = _rootDirectoryCache;
|
|
string targetName = pathComponents[^1]; // Last component is the target name
|
|
|
|
// Traverse all but the last component
|
|
for(var i = 0; i < pathComponents.Length - 1; i++)
|
|
{
|
|
string component = pathComponents[i];
|
|
AaruLogging.Debug(MODULE_NAME, "Navigating to component '{0}'", component);
|
|
|
|
if(!currentEntries.TryGetValue(component, out long childInodeAddr))
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Component '{0}' not found", component);
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
// Read the child i-node
|
|
ErrorNumber errno = ReadInode(childInodeAddr, out Inode childInode);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading child i-node: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
if(!IsDirectory(childInode))
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Component '{0}' is not a directory", component);
|
|
|
|
return ErrorNumber.NotDirectory;
|
|
}
|
|
|
|
errno = ParseDirectoryBTree(childInode.data, out Dictionary<string, long> childEntries);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error parsing child directory: {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
currentEntries = childEntries;
|
|
}
|
|
|
|
// Find the target file/directory in the current directory
|
|
if(!currentEntries.TryGetValue(targetName, out long targetInodeAddr))
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Target '{0}' not found", targetName);
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
// Read the target i-node
|
|
AaruLogging.Debug(MODULE_NAME, "Target '{0}': i-node block address = {1}", targetName, targetInodeAddr);
|
|
|
|
ErrorNumber readError = ReadInode(targetInodeAddr, out Inode targetInode);
|
|
|
|
if(readError != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading target i-node: {0}", readError);
|
|
|
|
return readError;
|
|
}
|
|
|
|
// Validate inode magic
|
|
if(targetInode.magic1 != INODE_MAGIC)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Invalid i-node magic: 0x{0:X8}", targetInode.magic1);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Build FileEntryInfo from the i-node
|
|
// AtheOS timestamps are in microseconds since epoch
|
|
stat = new FileEntryInfo
|
|
{
|
|
Length = targetInode.data.size,
|
|
Inode = (ulong)targetInodeAddr,
|
|
UID = (ulong)targetInode.uid,
|
|
GID = (ulong)targetInode.gid,
|
|
CreationTimeUtc =
|
|
DateHandlers.UnixUnsignedToDateTime((ulong)(targetInode.create_time / 1000000))
|
|
.AddTicks(targetInode.create_time % 1000000 * 10),
|
|
LastWriteTimeUtc =
|
|
DateHandlers.UnixUnsignedToDateTime((ulong)(targetInode.modified_time / 1000000))
|
|
.AddTicks(targetInode.modified_time % 1000000 * 10),
|
|
AccessTimeUtc = DateHandlers.UnixUnsignedToDateTime((ulong)(targetInode.modified_time / 1000000))
|
|
.AddTicks(targetInode.modified_time % 1000000 * 10),
|
|
Mode = (uint)targetInode.mode,
|
|
Links = (ulong)targetInode.link_count
|
|
};
|
|
|
|
// Determine file type from mode field (Unix-style file type bits)
|
|
int fileType = targetInode.mode & 0xF000;
|
|
|
|
stat.Attributes = fileType switch
|
|
{
|
|
0x4000 => // S_IFDIR - Directory
|
|
FileAttributes.Directory,
|
|
0xA000 => // S_IFLNK - Symbolic link
|
|
FileAttributes.Symlink,
|
|
0x2000 => // S_IFCHR - Character device
|
|
FileAttributes.CharDevice,
|
|
0x6000 => // S_IFBLK - Block device
|
|
FileAttributes.BlockDevice,
|
|
0x1000 => // S_IFIFO - FIFO (named pipe)
|
|
FileAttributes.FIFO,
|
|
0xC000 => // S_IFSOCK - Socket
|
|
FileAttributes.Socket,
|
|
_ => FileAttributes.File
|
|
};
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Stat successful: name='{0}', size={1}, inode={2}, mode=0x{3:X}",
|
|
targetName,
|
|
stat.Length,
|
|
stat.Inode,
|
|
stat.Mode);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
} |