mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
506 lines
18 KiB
C#
506 lines
18 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : File.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Linux extended filesystem 2, 3 and 4 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 System.Text;
|
|
using Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Structs;
|
|
using Aaru.Helpers;
|
|
using Aaru.Logging;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
public sealed partial class ext2FS
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber Stat(string path, out FileEntryInfo stat)
|
|
{
|
|
stat = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
string normalizedPath = path ?? "/";
|
|
|
|
if(normalizedPath is "" or ".") normalizedPath = "/";
|
|
|
|
uint targetInodeNum;
|
|
|
|
// Root directory
|
|
if(normalizedPath is "/")
|
|
targetInodeNum = EXT2_ROOT_INO;
|
|
else
|
|
{
|
|
// Resolve path to inode number
|
|
ErrorNumber errno = ResolvePathToInode(normalizedPath, out targetInodeNum);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
}
|
|
|
|
// Read the target inode
|
|
ErrorNumber readError = ReadInode(targetInodeNum, out Inode inode);
|
|
|
|
if(readError != ErrorNumber.NoError) return readError;
|
|
|
|
stat = InodeToFileEntryInfo(inode, targetInodeNum);
|
|
|
|
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);
|
|
|
|
// Resolve path to inode
|
|
ErrorNumber errno = ResolvePathToInode(path, out uint inodeNumber);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile: ResolvePathToInode failed with {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Read the inode
|
|
errno = ReadInode(inodeNumber, out Inode inode);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile: ReadInode failed with {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Check it's not a directory
|
|
if((inode.mode & S_IFMT) == S_IFDIR)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile: path is a directory");
|
|
|
|
return ErrorNumber.IsDirectory;
|
|
}
|
|
|
|
// Pre-compute data block list
|
|
errno = GetInodeDataBlocks(inode, out List<(ulong physicalBlock, uint length)> blockList);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile: GetInodeDataBlocks failed with {0}", errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
ulong fileSize = (ulong)inode.size_high << 32 | inode.size_lo;
|
|
|
|
// Detect e2compr compressed file
|
|
bool isCompressed = (inode.i_flags & EXT2_COMPR_FL) != 0;
|
|
byte comprMethod = 0;
|
|
uint clusterNBlocks = 1;
|
|
|
|
if(isCompressed)
|
|
{
|
|
// Per-inode compression params are stored in the obsoleted fragment address field (obso_faddr)
|
|
// Bits 0-7: compression method, Bits 8-15: log2(cluster_nblocks)
|
|
comprMethod = (byte)(inode.obso_faddr & 0xFF);
|
|
var log2CluNBlocks = (byte)(inode.obso_faddr >> 8 & 0xFF);
|
|
|
|
// Default cluster size if not set
|
|
if(log2CluNBlocks == 0) log2CluNBlocks = 3; // 8 blocks per cluster
|
|
|
|
clusterNBlocks = 1u << log2CluNBlocks;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"OpenFile: compressed file, method={0}, log2_clu_nblocks={1}, clu_nblocks={2}",
|
|
comprMethod,
|
|
log2CluNBlocks,
|
|
clusterNBlocks);
|
|
}
|
|
|
|
node = new Ext2FileNode
|
|
{
|
|
Path = path,
|
|
Length = (long)fileSize,
|
|
Offset = 0,
|
|
InodeNumber = inodeNumber,
|
|
Inode = inode,
|
|
BlockList = blockList,
|
|
CachedBlock = null,
|
|
CachedBlockIndex = -1,
|
|
IsCompressed = isCompressed,
|
|
CompressionMethod = comprMethod,
|
|
ClusterNBlocks = clusterNBlocks
|
|
};
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "OpenFile: success, inode={0}, size={1}", inodeNumber, fileSize);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseFile(IFileNode node)
|
|
{
|
|
if(node is not Ext2FileNode fileNode) return ErrorNumber.InvalidArgument;
|
|
|
|
fileNode.CachedBlock = null;
|
|
fileNode.CachedBlockIndex = -1;
|
|
fileNode.DecompressedClusterCache.Clear();
|
|
|
|
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 Ext2FileNode 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);
|
|
|
|
long bytesRead = 0;
|
|
long currentOffset = fileNode.Offset;
|
|
|
|
while(bytesRead < toRead)
|
|
{
|
|
// Calculate which logical block contains the current offset
|
|
long blockIndex = currentOffset / _blockSize;
|
|
var offsetInBlock = (int)(currentOffset % _blockSize);
|
|
|
|
byte[] blockData;
|
|
|
|
// Use cached block if it matches
|
|
if(blockIndex == fileNode.CachedBlockIndex && fileNode.CachedBlock != null)
|
|
blockData = fileNode.CachedBlock;
|
|
else
|
|
{
|
|
ErrorNumber errno;
|
|
|
|
// For compressed files, use cluster-aware reading with decompression
|
|
if(fileNode.IsCompressed)
|
|
errno = ReadCompressedLogicalBlock(fileNode, (ulong)blockIndex, out blockData);
|
|
else
|
|
errno = ReadLogicalBlock(fileNode.BlockList, (ulong)blockIndex, out blockData);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadFile: ReadLogicalBlock failed for block {0}: {1}",
|
|
blockIndex,
|
|
errno);
|
|
|
|
if(bytesRead > 0) break;
|
|
|
|
return errno;
|
|
}
|
|
|
|
// Cache the block
|
|
fileNode.CachedBlock = blockData;
|
|
fileNode.CachedBlockIndex = blockIndex;
|
|
}
|
|
|
|
if(blockData == null || blockData.Length == 0)
|
|
{
|
|
// Sparse block — fill with zeros
|
|
long bytesToZero = Math.Min(_blockSize - offsetInBlock, toRead - bytesRead);
|
|
Array.Clear(buffer, (int)bytesRead, (int)bytesToZero);
|
|
bytesRead += bytesToZero;
|
|
currentOffset += bytesToZero;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Copy data from block to buffer
|
|
long bytesToCopy = Math.Min(blockData.Length - offsetInBlock, toRead - bytesRead);
|
|
Array.Copy(blockData, offsetInBlock, buffer, bytesRead, bytesToCopy);
|
|
|
|
bytesRead += bytesToCopy;
|
|
currentOffset += bytesToCopy;
|
|
}
|
|
|
|
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;
|
|
|
|
if(string.IsNullOrEmpty(path) || path == "/") return ErrorNumber.InvalidArgument;
|
|
|
|
// Resolve path to inode
|
|
ErrorNumber errno = ResolvePathToInode(path, out uint inodeNumber);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Read the inode
|
|
errno = ReadInode(inodeNumber, out Inode inode);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Verify it's a symlink
|
|
if((inode.mode & S_IFMT) != S_IFLNK) return ErrorNumber.InvalidArgument;
|
|
|
|
ulong linkSize = (ulong)inode.size_high << 32 | inode.size_lo;
|
|
|
|
if(linkSize == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
// Fast symlink: target stored inline in inode.block[] (60 bytes)
|
|
// Detected by having no allocated blocks
|
|
ulong totalBlocks = (ulong)inode.blocks_high << 32 | inode.blocks_lo;
|
|
|
|
if(totalBlocks == 0 && linkSize < 60)
|
|
{
|
|
var linkData = new byte[linkSize];
|
|
var blockBytes = new byte[60];
|
|
|
|
for(var i = 0; i < 15; i++)
|
|
{
|
|
byte[] b = BitConverter.GetBytes(inode.block[i]);
|
|
Array.Copy(b, 0, blockBytes, i * 4, 4);
|
|
}
|
|
|
|
Array.Copy(blockBytes, 0, linkData, 0, (int)linkSize);
|
|
dest = StringHandlers.CToString(linkData, _encoding ?? Encoding.UTF8);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadLink: fast symlink inode={0}, target='{1}'", inodeNumber, dest);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Slow symlink: target stored in data blocks
|
|
errno = GetInodeDataBlocks(inode, out List<(ulong physicalBlock, uint length)> blockList);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
var targetData = new byte[linkSize];
|
|
ulong bytesRead = 0;
|
|
|
|
foreach((ulong physicalBlock, uint length) in blockList)
|
|
{
|
|
if(bytesRead >= linkSize) break;
|
|
|
|
var toRead = (uint)Math.Min(length * _blockSize, linkSize - bytesRead);
|
|
|
|
errno = ReadBytes(physicalBlock * _blockSize, toRead, out byte[] blockData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
var toCopy = (uint)Math.Min((ulong)blockData.Length, linkSize - bytesRead);
|
|
Array.Copy(blockData, 0, targetData, (int)bytesRead, (int)toCopy);
|
|
bytesRead += toCopy;
|
|
}
|
|
|
|
dest = StringHandlers.CToString(targetData, _encoding ?? Encoding.UTF8);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadLink: slow symlink inode={0}, target='{1}'", inodeNumber, dest);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Resolves a filesystem path to its inode number</summary>
|
|
/// <param name="path">The path to resolve</param>
|
|
/// <param name="inodeNumber">The resolved inode number</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber ResolvePathToInode(string path, out uint inodeNumber)
|
|
{
|
|
inodeNumber = EXT2_ROOT_INO;
|
|
|
|
string stripped = path.StartsWith("/", StringComparison.Ordinal) ? path[1..] : path;
|
|
|
|
string[] components = stripped.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if(components.Length == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
Dictionary<string, uint> currentEntries = _rootDirectoryCache;
|
|
|
|
for(var i = 0; i < components.Length; i++)
|
|
{
|
|
string component = components[i];
|
|
|
|
if(component is "." or "..") continue;
|
|
|
|
if(!currentEntries.TryGetValue(component, out uint childInode)) return ErrorNumber.NoSuchFile;
|
|
|
|
// Last component — found it
|
|
if(i == components.Length - 1)
|
|
{
|
|
inodeNumber = childInode;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Intermediate — must be a directory, read its entries
|
|
ErrorNumber errno = ReadInode(childInode, out Inode inode);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
if((inode.mode & S_IFMT) != S_IFDIR) return ErrorNumber.NotDirectory;
|
|
|
|
ulong dirSize = (ulong)inode.size_high << 32 | inode.size_lo;
|
|
|
|
errno = ReadDirectoryEntries(inode, dirSize, out Dictionary<string, uint> subEntries);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
currentEntries = subEntries.Where(e => e.Key is not ("." or "..")).ToDictionary(e => e.Key, e => e.Value);
|
|
}
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
/// <summary>Converts an ext2/3/4 inode to a <see cref="FileEntryInfo" /></summary>
|
|
/// <param name="inode">The inode structure</param>
|
|
/// <param name="inodeNumber">The inode number</param>
|
|
/// <returns>The file entry info</returns>
|
|
FileEntryInfo InodeToFileEntryInfo(Inode inode, uint inodeNumber)
|
|
{
|
|
ulong fileSize = (ulong)inode.size_high << 32 | inode.size_lo;
|
|
|
|
FileEntryInfo info = new()
|
|
{
|
|
Inode = inodeNumber,
|
|
Links = inode.links_count,
|
|
Length = (long)fileSize,
|
|
BlockSize = _blockSize,
|
|
Blocks = (long)((fileSize + _blockSize - 1) / _blockSize),
|
|
UID = (ulong)inode.uid_high << 16 | inode.uid,
|
|
GID = (ulong)inode.gid_high << 16 | inode.gid,
|
|
Mode = inode.mode,
|
|
|
|
// Standard timestamps (seconds since Unix epoch)
|
|
AccessTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.atime),
|
|
LastWriteTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.mtime),
|
|
StatusChangeTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.ctime)
|
|
};
|
|
|
|
// Extended timestamps with nanosecond precision (when inode size > 128)
|
|
if(_inodeSize > EXT2_GOOD_OLD_INODE_SIZE && inode.extra_isize > 0)
|
|
{
|
|
// Creation time (crtime) is only present in extended inodes
|
|
if(inode.crtime != 0 || inode.crtime_extra != 0)
|
|
info.CreationTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.crtime, inode.crtime_extra >> 2);
|
|
|
|
// Override standard timestamps with extended precision when available
|
|
if(inode.atime_extra != 0)
|
|
info.AccessTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.atime, inode.atime_extra >> 2);
|
|
|
|
if(inode.mtime_extra != 0)
|
|
info.LastWriteTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.mtime, inode.mtime_extra >> 2);
|
|
|
|
if(inode.ctime_extra != 0)
|
|
info.StatusChangeTimeUtc = DateHandlers.UnixUnsignedToDateTime(inode.ctime, inode.ctime_extra >> 2);
|
|
}
|
|
|
|
// Determine file type from mode
|
|
info.Attributes = (inode.mode & S_IFMT) switch
|
|
{
|
|
S_IFDIR => FileAttributes.Directory,
|
|
S_IFLNK => FileAttributes.Symlink,
|
|
S_IFCHR => FileAttributes.CharDevice,
|
|
S_IFBLK => FileAttributes.BlockDevice,
|
|
S_IFIFO => FileAttributes.Pipe,
|
|
S_IFSOCK => FileAttributes.Socket,
|
|
_ => FileAttributes.File
|
|
};
|
|
|
|
// Map ext2/ext4 inode flags to FileAttributes
|
|
if((inode.i_flags & EXT2_APPEND_FL) != 0) info.Attributes |= FileAttributes.AppendOnly;
|
|
|
|
if((inode.i_flags & EXT2_IMMUTABLE_FL) != 0) info.Attributes |= FileAttributes.Immutable;
|
|
|
|
if((inode.i_flags & EXT2_NODUMP_FL) != 0) info.Attributes |= FileAttributes.NoDump;
|
|
|
|
if((inode.i_flags & EXT2_NOATIME_FL) != 0) info.Attributes |= FileAttributes.NoAccessTime;
|
|
|
|
if((inode.i_flags & EXT2_SYNC_FL) != 0) info.Attributes |= FileAttributes.Sync;
|
|
|
|
if((inode.i_flags & EXT2_COMPR_FL) != 0) info.Attributes |= FileAttributes.Compressed;
|
|
|
|
if((inode.i_flags & EXT2_ECOMPR_FL) != 0) info.Attributes |= FileAttributes.CompressionError;
|
|
|
|
if((inode.i_flags & EXT4_EXTENTS_FL) != 0) info.Attributes |= FileAttributes.Extents;
|
|
|
|
if((inode.i_flags & EXT2_INDEX_FL) != 0) info.Attributes |= FileAttributes.IndexedDirectory;
|
|
|
|
if((inode.i_flags & EXT3_JOURNAL_DATA_FL) != 0) info.Attributes |= FileAttributes.Journaled;
|
|
|
|
if((inode.i_flags & EXT2_TOPDIR_FL) != 0) info.Attributes |= FileAttributes.TopDirectory;
|
|
|
|
if((inode.i_flags & EXT4_INLINE_DATA_FL) != 0) info.Attributes |= FileAttributes.Inline;
|
|
|
|
if((inode.i_flags & EXT4_ENCRYPT_FL) != 0) info.Attributes |= FileAttributes.Encrypted;
|
|
|
|
// Device number for block/char devices
|
|
if((inode.mode & S_IFMT) is S_IFCHR or S_IFBLK)
|
|
{
|
|
// Device number is stored in block[0] (old format) or block[1] (new format)
|
|
uint dev = inode.block[0];
|
|
|
|
if(dev == 0 && inode.block[1] != 0) dev = inode.block[1];
|
|
|
|
// Linux new device format: major = bits 8-19, minor = bits 0-7 + bits 20-31
|
|
uint major = dev >> 8 & 0xFFF;
|
|
uint minor = dev & 0xFF | dev >> 12 & 0xFFF00;
|
|
|
|
info.DeviceNo = (ulong)major << 32 | minor;
|
|
}
|
|
|
|
return info;
|
|
}
|
|
} |