Files
Aaru/Aaru.Filesystems/XFS/File.cs

577 lines
21 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : File.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : XFS 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.Helpers;
using Aaru.Logging;
using Marshal = Aaru.Helpers.Marshal;
namespace Aaru.Filesystems;
public sealed partial class XFS
{
/// <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 path
string normalizedPath = path ?? "/";
if(normalizedPath is "" or ".") normalizedPath = "/";
// Root directory handling
if(normalizedPath == "/")
{
ErrorNumber errno = ReadInode(_superblock.rootino, out Dinode rootInode);
if(errno != ErrorNumber.NoError) return errno;
stat = InodeToFileEntryInfo(rootInode, _superblock.rootino);
return ErrorNumber.NoError;
}
// Traverse the path to find the target inode
ErrorNumber lookupErrno = LookupInode(normalizedPath, out ulong inodeNumber);
if(lookupErrno != ErrorNumber.NoError) return lookupErrno;
ErrorNumber readErrno = ReadInode(inodeNumber, out Dinode inode);
if(readErrno != ErrorNumber.NoError) return readErrno;
stat = InodeToFileEntryInfo(inode, inodeNumber);
AaruLogging.Debug(MODULE_NAME,
"Stat successful: path='{0}', size={1}, inode={2}, mode=0x{3:X4}",
normalizedPath,
stat.Length,
stat.Inode,
stat.Mode);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber OpenFile(string path, out IFileNode node)
{
node = null;
if(!_mounted) return ErrorNumber.AccessDenied;
if(string.IsNullOrEmpty(path) || path == "/") return ErrorNumber.IsDirectory;
AaruLogging.Debug(MODULE_NAME, "OpenFile: path='{0}'", path);
// Verify the file exists and get its stat info
ErrorNumber errno = Stat(path, out FileEntryInfo stat);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "OpenFile: Stat failed with {0}", errno);
return errno;
}
// Reject directories
if(stat.Attributes.HasFlag(FileAttributes.Directory))
{
AaruLogging.Debug(MODULE_NAME, "OpenFile: path is a directory");
return ErrorNumber.IsDirectory;
}
// Look up inode number
errno = LookupInode(path, out ulong inodeNumber);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "OpenFile: LookupInode failed with {0}", errno);
return errno;
}
// Read the inode
errno = ReadInode(inodeNumber, out Dinode inode);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "OpenFile: ReadInode failed with {0}", errno);
return errno;
}
// Load extent map
errno = LoadFileExtents(inodeNumber, inode, out XfsExtent[] extents);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "OpenFile: LoadFileExtents failed with {0}", errno);
return errno;
}
node = new XfsFileNode
{
Path = path,
Length = inode.di_size,
Offset = 0,
InodeNumber = inodeNumber,
Inode = inode,
Extents = extents
};
AaruLogging.Debug(MODULE_NAME, "OpenFile: success, size={0}, extents={1}", inode.di_size, extents.Length);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber CloseFile(IFileNode node)
{
if(!_mounted) return ErrorNumber.AccessDenied;
if(node is not XfsFileNode xfsNode) return ErrorNumber.InvalidArgument;
xfsNode.Extents = null;
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 XfsFileNode fileNode) return ErrorNumber.InvalidArgument;
if(buffer == null) return ErrorNumber.InvalidArgument;
if(fileNode.Offset < 0 || fileNode.Offset >= fileNode.Length) return ErrorNumber.InvalidArgument;
// Clamp read length to remaining file size and buffer size
long toRead = length;
if(fileNode.Offset + toRead > fileNode.Length) toRead = fileNode.Length - fileNode.Offset;
if(toRead <= 0) return ErrorNumber.NoError;
if(toRead > buffer.Length) toRead = buffer.Length;
AaruLogging.Debug(MODULE_NAME, "ReadFile: offset={0}, length={1}, toRead={2}", fileNode.Offset, length, toRead);
// Handle inline (local format) data stored directly in the inode fork
if(fileNode.Inode.di_format == XFS_DINODE_FMT_LOCAL)
return ReadInlineFileData(fileNode, toRead, buffer, out read);
// Normal extent-based file reading
uint blockSize = _superblock.blocksize;
long bytesRead = 0;
long currentOffset = fileNode.Offset;
while(bytesRead < toRead)
{
// Calculate the logical file block number and offset within that block
var logicalBlock = (ulong)(currentOffset / blockSize);
var offsetInBlock = (int)(currentOffset % blockSize);
// Find the extent containing this logical block
var found = false;
foreach(XfsExtent extent in fileNode.Extents)
{
if(logicalBlock < extent.StartOff || logicalBlock >= extent.StartOff + extent.BlockCount) continue;
long bytesToCopy = Math.Min(blockSize - offsetInBlock, toRead - bytesRead);
// Unwritten (preallocated) extents return zeros
if(extent.Unwritten)
{
Array.Clear(buffer, (int)bytesRead, (int)bytesToCopy);
bytesRead += bytesToCopy;
currentOffset += bytesToCopy;
found = true;
break;
}
// Calculate the physical filesystem block
ulong blockInExtent = logicalBlock - extent.StartOff;
ulong physBlock = extent.StartBlock + blockInExtent;
ErrorNumber errno = ReadBlock(physBlock, out byte[] blockData);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "ReadFile: ReadBlock failed for block {0}: {1}", physBlock, errno);
if(bytesRead > 0) goto done;
return errno;
}
Array.Copy(blockData, offsetInBlock, buffer, bytesRead, bytesToCopy);
bytesRead += bytesToCopy;
currentOffset += bytesToCopy;
found = true;
break;
}
if(!found)
{
// Sparse hole — fill with zeros
long bytesToCopy = Math.Min(blockSize - offsetInBlock, toRead - bytesRead);
Array.Clear(buffer, (int)bytesRead, (int)bytesToCopy);
bytesRead += bytesToCopy;
currentOffset += bytesToCopy;
}
}
done:
read = bytesRead;
fileNode.Offset += bytesRead;
AaruLogging.Debug(MODULE_NAME, "ReadFile: read {0} bytes, new offset={1}", read, fileNode.Offset);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber ReadLink(string path, out string dest)
{
dest = null;
if(!_mounted) return ErrorNumber.AccessDenied;
AaruLogging.Debug(MODULE_NAME, "ReadLink: path='{0}'", path);
// Verify it exists and is a symlink
ErrorNumber errno = Stat(path, out FileEntryInfo stat);
if(errno != ErrorNumber.NoError) return errno;
if(!stat.Attributes.HasFlag(FileAttributes.Symlink)) return ErrorNumber.InvalidArgument;
// Look up the inode
errno = LookupInode(path, out ulong inodeNumber);
if(errno != ErrorNumber.NoError) return errno;
// Read the inode
errno = ReadInode(inodeNumber, out Dinode inode);
if(errno != ErrorNumber.NoError) return errno;
if(inode.di_size <= 0 || inode.di_size > 1024)
{
AaruLogging.Debug(MODULE_NAME, "ReadLink: invalid symlink size {0}", inode.di_size);
return ErrorNumber.InvalidArgument;
}
// Inline symlink: target stored directly in the inode data fork
if(inode.di_format == XFS_DINODE_FMT_LOCAL)
{
errno = ReadInodeRaw(inodeNumber, out byte[] rawInode);
if(errno != ErrorNumber.NoError) return errno;
int coreSize = _v3Inodes ? Marshal.SizeOf<Dinode>() : 100;
var dataLen = (int)inode.di_size;
if(coreSize + dataLen > rawInode.Length) dataLen = rawInode.Length - coreSize;
dest = _encoding.GetString(rawInode, coreSize, dataLen).TrimEnd('\0');
return ErrorNumber.NoError;
}
// Remote symlink: target stored in extent/btree data blocks
errno = LoadFileExtents(inodeNumber, inode, out XfsExtent[] extents);
if(errno != ErrorNumber.NoError) return errno;
uint blockSize = _superblock.blocksize;
var pathLen = (int)inode.di_size;
var linkData = new byte[pathLen];
var offset = 0;
// V5 (CRC) filesystems have a symlink header per block
int headerSize = _v3Inodes ? Marshal.SizeOf<SymlinkHeader>() : 0;
foreach(XfsExtent extent in extents)
{
for(uint b = 0; b < extent.BlockCount && offset < pathLen; b++)
{
errno = ReadBlock(extent.StartBlock + b, out byte[] blockData);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME,
"ReadLink: ReadBlock failed for block {0}: {1}",
extent.StartBlock + b,
errno);
return errno;
}
int dataOffset = headerSize;
int available = (int)blockSize - headerSize;
if(available > pathLen - offset) available = pathLen - offset;
Array.Copy(blockData, dataOffset, linkData, offset, available);
offset += available;
}
}
dest = _encoding.GetString(linkData, 0, pathLen).TrimEnd('\0');
AaruLogging.Debug(MODULE_NAME, "ReadLink: target='{0}'", dest);
return ErrorNumber.NoError;
}
/// <summary>Looks up an inode number by traversing a path from the root directory</summary>
/// <param name="path">Absolute path to look up</param>
/// <param name="inodeNumber">Output inode number</param>
/// <returns>Error number indicating success or failure</returns>
ErrorNumber LookupInode(string path, out ulong inodeNumber)
{
inodeNumber = 0;
string normalizedPath = path ?? "/";
if(normalizedPath is "" or "." or "/")
{
inodeNumber = _superblock.rootino;
return ErrorNumber.NoError;
}
string pathWithoutLeadingSlash = normalizedPath.StartsWith("/", StringComparison.Ordinal)
? normalizedPath[1..]
: normalizedPath;
string[] pathComponents = pathWithoutLeadingSlash.Split('/', StringSplitOptions.RemoveEmptyEntries);
if(pathComponents.Length == 0) return ErrorNumber.InvalidArgument;
// Start from root directory cache
Dictionary<string, ulong> currentEntries = _rootDirectoryCache;
for(var p = 0; p < pathComponents.Length; p++)
{
string component = pathComponents[p];
if(component is "." or "..") continue;
if(!currentEntries.TryGetValue(component, out ulong foundIno))
{
AaruLogging.Debug(MODULE_NAME, "LookupInode: Component '{0}' not found", component);
return ErrorNumber.NoSuchFile;
}
// If this is the last component, we found it
if(p == pathComponents.Length - 1)
{
inodeNumber = foundIno;
return ErrorNumber.NoError;
}
// Not the last component — must be a directory, read its contents
ErrorNumber errno = ReadInode(foundIno, out Dinode inode);
if(errno != ErrorNumber.NoError) return errno;
if((inode.di_mode & S_IFMT) != S_IFDIR)
{
AaruLogging.Debug(MODULE_NAME, "LookupInode: '{0}' is not a directory", component);
return ErrorNumber.NotDirectory;
}
errno = GetDirectoryContents(foundIno, inode, out Dictionary<string, ulong> dirEntries);
if(errno != ErrorNumber.NoError) return errno;
currentEntries = dirEntries;
}
return ErrorNumber.NoSuchFile;
}
/// <summary>Converts an XFS dinode to a FileEntryInfo structure</summary>
/// <param name="inode">The dinode</param>
/// <param name="inodeNumber">The inode number</param>
/// <returns>The populated FileEntryInfo</returns>
FileEntryInfo InodeToFileEntryInfo(Dinode inode, ulong inodeNumber)
{
// V1 inodes store link count in di_onlink (16-bit, mapped to di_metatype);
// V2+ inodes store link count in di_nlink (32-bit)
uint linkCount = inode.di_version == 1 ? inode.di_metatype : inode.di_nlink;
var info = new FileEntryInfo
{
Attributes = FileAttributes.None,
BlockSize = _superblock.blocksize,
Inode = inodeNumber,
Length = inode.di_size,
Links = linkCount,
UID = inode.di_uid,
GID = inode.di_gid,
Mode = (uint)(inode.di_mode & 0x0FFF),
Blocks = inode.di_nblocks,
AccessTimeUtc = XfsTimestampToDateTime(inode, inode.di_atime),
LastWriteTimeUtc = XfsTimestampToDateTime(inode, inode.di_mtime),
StatusChangeTimeUtc = XfsTimestampToDateTime(inode, inode.di_ctime)
};
// v3 inodes have a creation time
if(_v3Inodes) info.CreationTimeUtc = XfsTimestampToDateTime(inode, inode.di_crtime);
// Determine file type from di_mode
info.Attributes = (inode.di_mode & S_IFMT) switch
{
0x4000 => FileAttributes.Directory, // S_IFDIR
0x8000 => FileAttributes.File, // S_IFREG
0xA000 => FileAttributes.Symlink, // S_IFLNK
0x2000 => FileAttributes.CharDevice, // S_IFCHR
0x6000 => FileAttributes.BlockDevice, // S_IFBLK
0x1000 => FileAttributes.FIFO, // S_IFIFO
0xC000 => FileAttributes.Socket, // S_IFSOCK
_ => FileAttributes.File
};
// XFS inode flags → file attributes
if((inode.di_flags & XFS_DIFLAG_IMMUTABLE) != 0) info.Attributes |= FileAttributes.Immutable;
if((inode.di_flags & XFS_DIFLAG_APPEND) != 0) info.Attributes |= FileAttributes.AppendOnly;
// Extract device major/minor for char and block device inodes
// The 32-bit SYSV dev_t is stored at the start of the data fork (FMT_DEV)
if((inode.di_mode & S_IFMT) is 0x2000 or 0x6000 && inode.di_format == XFS_DINODE_FMT_DEV)
{
ErrorNumber devErrno = ReadInodeRaw(inodeNumber, out byte[] rawInode);
if(devErrno == ErrorNumber.NoError)
{
int coreSize = _v3Inodes ? Marshal.SizeOf<Dinode>() : 100;
if(rawInode.Length >= coreSize + 4)
{
var sysvDev = BigEndianBitConverter.ToUInt32(rawInode, coreSize);
uint major = sysvDev >> 8 & 0xFF;
uint minor = sysvDev & 0xFF;
info.DeviceNo = (ulong)major << 32 | minor;
}
}
}
return info;
}
/// <summary>
/// Converts an XFS on-disk timestamp to a DateTime in UTC, handling both legacy and bigtime formats.
/// Legacy format: upper 32 bits = seconds (signed), lower 32 bits = nanoseconds.
/// Bigtime format: single uint64 of nanoseconds since the bigtime epoch
/// (Unix epoch minus 2^31 seconds, i.e. XFS_BIGTIME_EPOCH_OFFSET = -(int64)INT32_MIN = 2147483648).
/// </summary>
/// <param name="inode">The dinode (needed to check bigtime flag)</param>
/// <param name="xfsTimestamp">The packed XFS timestamp</param>
/// <returns>DateTime in UTC</returns>
DateTime XfsTimestampToDateTime(Dinode inode, long xfsTimestamp)
{
// Bigtime: di_version >= 3 && (di_flags2 & XFS_DIFLAG2_BIGTIME) != 0
if(_v3Inodes && (inode.di_flags2 & XFS_DIFLAG2_BIGTIME) != 0)
{
// The 64-bit value is total nanoseconds since the bigtime epoch.
// bigtime_epoch = unix_epoch - 2147483648 seconds
// unix_seconds = (ts / 1_000_000_000) - 2147483648
var totalNs = (ulong)xfsTimestamp;
ulong seconds = totalNs / 1_000_000_000UL;
ulong nanosRem = totalNs % 1_000_000_000UL;
long unixSeconds = (long)seconds - 2147483648L;
return DateHandlers.UnixToDateTime(unixSeconds).AddTicks((long)nanosRem / 100);
}
// Legacy format: upper 32 = signed seconds, lower 32 = unsigned nanoseconds
var legacySeconds = (int)(xfsTimestamp >> 32);
var legacyNanoseconds = (int)(xfsTimestamp & 0xFFFFFFFF);
return DateHandlers.UnixToDateTime(legacySeconds).AddTicks(legacyNanoseconds / 100);
}
/// <summary>Reads data from a file stored inline in the inode data fork (local format)</summary>
/// <param name="fileNode">The file node</param>
/// <param name="toRead">Number of bytes to read</param>
/// <param name="buffer">Output buffer</param>
/// <param name="read">Actual bytes read</param>
/// <returns>Error number indicating success or failure</returns>
ErrorNumber ReadInlineFileData(XfsFileNode fileNode, long toRead, byte[] buffer, out long read)
{
read = 0;
ErrorNumber errno = ReadInodeRaw(fileNode.InodeNumber, out byte[] rawInode);
if(errno != ErrorNumber.NoError) return errno;
int coreSize = _v3Inodes ? Marshal.SizeOf<Dinode>() : 100;
var dataLen = (int)fileNode.Inode.di_size;
if(coreSize + dataLen > rawInode.Length) dataLen = rawInode.Length - coreSize;
var copySize = (int)Math.Min(toRead, dataLen - fileNode.Offset);
if(copySize <= 0) return ErrorNumber.NoError;
Array.Copy(rawInode, coreSize + (int)fileNode.Offset, buffer, 0, copySize);
read = copySize;
fileNode.Offset += copySize;
return ErrorNumber.NoError;
}
}