diff --git a/Aaru.Filesystems/Cram/Block.cs b/Aaru.Filesystems/Cram/Block.cs index a3e6c6f5a..2e7206043 100644 --- a/Aaru.Filesystems/Cram/Block.cs +++ b/Aaru.Filesystems/Cram/Block.cs @@ -27,7 +27,10 @@ // ****************************************************************************/ using System; +using System.IO; +using System.IO.Compression; using Aaru.CommonTypes.Enums; +using Aaru.Logging; namespace Aaru.Filesystems; @@ -69,4 +72,182 @@ public sealed partial class Cram return ErrorNumber.NoError; } + + /// Reads and decompresses a single block from a file + /// The file node + /// Block index (0-based) + /// Output decompressed block data + /// Error number indicating success or failure + ErrorNumber ReadBlock(CramFileNode fileNode, int blockIndex, out byte[] blockData) + { + blockData = null; + + if(blockIndex < 0 || blockIndex >= fileNode.BlockCount) return ErrorNumber.InvalidArgument; + + // Read the block pointer + uint blkPtrOffset = fileNode.BlockPtrOffset + (uint)(blockIndex * 4); + + ErrorNumber errno = ReadBytes(blkPtrOffset, 4, out byte[] ptrData); + + if(errno != ErrorNumber.NoError) return errno; + + uint blockPtr = _littleEndian + ? BitConverter.ToUInt32(ptrData, 0) + : (uint)(ptrData[0] << 24 | ptrData[1] << 16 | ptrData[2] << 8 | ptrData[3]); + + bool uncompressed = (blockPtr & CRAMFS_BLK_FLAG_UNCOMPRESSED) != 0; + bool direct = (blockPtr & CRAMFS_BLK_FLAG_DIRECT_PTR) != 0; + + blockPtr &= ~CRAMFS_BLK_FLAGS; + + uint blockStart; + uint blockLen; + + if(direct) + { + // Direct pointer: absolute start pointer shifted by 2 bits + blockStart = blockPtr << CRAMFS_BLK_DIRECT_PTR_SHIFT; + + if(uncompressed) + { + // Uncompressed: size is PAGE_SIZE (or less for last block) + blockLen = PAGE_SIZE; + + if(blockIndex == fileNode.BlockCount - 1) + { + // Last block: cap to remaining file length + blockLen = (uint)(fileNode.Length % PAGE_SIZE); + + if(blockLen == 0) blockLen = PAGE_SIZE; + } + } + else + { + // Compressed: size is in first 2 bytes + errno = ReadBytes(blockStart, 2, out byte[] sizeData); + + if(errno != ErrorNumber.NoError) return errno; + + blockLen = _littleEndian ? BitConverter.ToUInt16(sizeData, 0) : (uint)(sizeData[0] << 8 | sizeData[1]); + + blockStart += 2; + } + } + else + { + // Non-direct: block pointer indicates end of current block + // Start comes from end of block pointer table (for first block) or previous block's pointer + blockStart = fileNode.BlockPtrOffset + fileNode.BlockCount * 4; + + if(blockIndex > 0) + { + // Read previous block's pointer to get start + errno = ReadBytes(blkPtrOffset - 4, 4, out byte[] prevPtrData); + + if(errno != ErrorNumber.NoError) return errno; + + uint prevPtr = _littleEndian + ? BitConverter.ToUInt32(prevPtrData, 0) + : (uint)(prevPtrData[0] << 24 | + prevPtrData[1] << 16 | + prevPtrData[2] << 8 | + prevPtrData[3]); + + // Handle case where previous pointer is direct + if((prevPtr & CRAMFS_BLK_FLAG_DIRECT_PTR) != 0) + { + uint prevStart = (prevPtr & ~CRAMFS_BLK_FLAGS) << CRAMFS_BLK_DIRECT_PTR_SHIFT; + + if((prevPtr & CRAMFS_BLK_FLAG_UNCOMPRESSED) != 0) + blockStart = prevStart + PAGE_SIZE; + else + { + // Read previous block's size + errno = ReadBytes(prevStart, 2, out byte[] prevSizeData); + + if(errno != ErrorNumber.NoError) return errno; + + uint prevLen = _littleEndian + ? BitConverter.ToUInt16(prevSizeData, 0) + : (uint)(prevSizeData[0] << 8 | prevSizeData[1]); + + blockStart = prevStart + 2 + prevLen; + } + } + else + blockStart = prevPtr & ~CRAMFS_BLK_FLAGS; + } + + blockLen = (blockPtr & ~CRAMFS_BLK_FLAGS) - blockStart; + } + + // Handle hole (zero-length block) + if(blockLen == 0) + { + blockData = new byte[PAGE_SIZE]; + + return ErrorNumber.NoError; + } + + // Sanity check block length + if(blockLen > 2 * PAGE_SIZE || uncompressed && blockLen > PAGE_SIZE) + { + AaruLogging.Debug(MODULE_NAME, "ReadBlock: bad block length {0}", blockLen); + + return ErrorNumber.InvalidArgument; + } + + // Read the block data + errno = ReadBytes(blockStart, blockLen, out byte[] compressedData); + + if(errno != ErrorNumber.NoError) return errno; + + if(uncompressed) + { + // Pad to PAGE_SIZE if needed + if(compressedData.Length >= PAGE_SIZE) + { + blockData = compressedData; + + return ErrorNumber.NoError; + } + + blockData = new byte[PAGE_SIZE]; + Array.Copy(compressedData, blockData, compressedData.Length); + + return ErrorNumber.NoError; + } + + // Decompress data + // Standard cramfs uses zlib compression (with header, not raw deflate) + // Note: The CramCompression enum exists for potential future extensions, + // but standard cramfs only uses zlib. There's no flag in the superblock + // to indicate alternative compression methods. + try + { + blockData = new byte[PAGE_SIZE]; + + using var compressedStream = new MemoryStream(compressedData); + using var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress); + + var totalRead = 0; + + while(totalRead < PAGE_SIZE) + { + int bytesDecompressed = zlibStream.Read(blockData, totalRead, PAGE_SIZE - totalRead); + + if(bytesDecompressed == 0) break; + + totalRead += bytesDecompressed; + } + + return ErrorNumber.NoError; + } + catch(Exception ex) + { + AaruLogging.Debug(MODULE_NAME, "ReadBlock: decompression failed: {0}", ex); + + return ErrorNumber.InvalidArgument; + } + } } \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Consts.cs b/Aaru.Filesystems/Cram/Consts.cs index 0ba772b91..dadc68281 100644 --- a/Aaru.Filesystems/Cram/Consts.cs +++ b/Aaru.Filesystems/Cram/Consts.cs @@ -97,5 +97,43 @@ public sealed partial class Cram /// const int CRAMFS_BLK_DIRECT_PTR_SHIFT = 2; +#endregion + +#region POSIX file type constants + + /// File type mask + const ushort S_IFMT = 0xF000; + + /// Socket + const ushort S_IFSOCK = 0xC000; + + /// Symbolic link + const ushort S_IFLNK = 0xA000; + + /// Regular file + const ushort S_IFREG = 0x8000; + + /// Block device + const ushort S_IFBLK = 0x6000; + + /// Directory + const ushort S_IFDIR = 0x4000; + + /// Character device + const ushort S_IFCHR = 0x2000; + + /// FIFO (named pipe) + const ushort S_IFIFO = 0x1000; + + /// Permission bits mask + const ushort S_IPERM = 0x0FFF; + +#endregion + +#region Page size + + /// CramFS page size (4KB) + const int PAGE_SIZE = 4096; + #endregion } \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/File.cs b/Aaru.Filesystems/Cram/File.cs index 5fb12d484..ea63b907a 100644 --- a/Aaru.Filesystems/Cram/File.cs +++ b/Aaru.Filesystems/Cram/File.cs @@ -29,27 +29,16 @@ using System; using System.Collections.Generic; using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Logging; +using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes; namespace Aaru.Filesystems; /// public sealed partial class Cram { - // POSIX file type constants (from mode) - const ushort S_IFMT = 0xF000; // File type mask - const ushort S_IFSOCK = 0xC000; // Socket - const ushort S_IFLNK = 0xA000; // Symbolic link - const ushort S_IFREG = 0x8000; // Regular file - const ushort S_IFBLK = 0x6000; // Block device - const ushort S_IFDIR = 0x4000; // Directory - const ushort S_IFCHR = 0x2000; // Character device - const ushort S_IFIFO = 0x1000; // FIFO (named pipe) - - // Permission mask - const ushort S_IPERM = 0x0FFF; // Permission bits - /// public ErrorNumber Stat(string path, out FileEntryInfo stat) { @@ -136,4 +125,277 @@ public sealed partial class Cram return ErrorNumber.NoSuchFile; } + + /// + 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); + + // Get file stat to verify it exists and determine its type + ErrorNumber errno = Stat(path, out FileEntryInfo stat); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "OpenFile: Stat failed with {0}", errno); + + return errno; + } + + // Check it's not a directory + if(stat.Attributes.HasFlag(FileAttributes.Directory)) + { + AaruLogging.Debug(MODULE_NAME, "OpenFile: path is a directory"); + + return ErrorNumber.IsDirectory; + } + + // Look up the file's inode + errno = LookupFile(path, out DirectoryEntryInfo entry); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "OpenFile: LookupFile failed with {0}", errno); + + return errno; + } + + uint size = entry.Inode.Size; + uint blockCount = (size + PAGE_SIZE - 1) / PAGE_SIZE; + + node = new CramFileNode + { + Path = path, + Length = size, + Offset = 0, + Inode = entry.Inode, + BlockPtrOffset = entry.Inode.Offset << 2, + BlockCount = blockCount + }; + + AaruLogging.Debug(MODULE_NAME, "OpenFile: success, size={0}, blocks={1}", size, blockCount); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber CloseFile(IFileNode node) + { + if(node is not CramFileNode fileNode) return ErrorNumber.InvalidArgument; + + // Clear node state to indicate it's closed + fileNode.Offset = -1; + fileNode.Length = 0; + fileNode.BlockPtrOffset = 0; + fileNode.BlockCount = 0; + fileNode.Path = null; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadLink(string path, out string dest) + { + dest = null; + + if(!_mounted) return ErrorNumber.AccessDenied; + + AaruLogging.Debug(MODULE_NAME, "ReadLink: path='{0}'", path); + + // Get file stat to verify it's 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 file's entry + errno = LookupFile(path, out DirectoryEntryInfo entry); + + if(errno != ErrorNumber.NoError) return errno; + + uint size = entry.Inode.Size; + + if(size == 0) + { + dest = ""; + + return ErrorNumber.NoError; + } + + // Read symlink target - symlinks are typically small + // Create a temporary file node to read the data + uint blockCount = (size + PAGE_SIZE - 1) / PAGE_SIZE; + + var tempNode = new CramFileNode + { + Path = path, + Length = size, + Offset = 0, + Inode = entry.Inode, + BlockPtrOffset = entry.Inode.Offset << 2, + BlockCount = blockCount + }; + + var linkData = new byte[size]; + var bytesRead = 0; + + // Read block by block + for(var blockIndex = 0; blockIndex < blockCount && bytesRead < size; blockIndex++) + { + errno = ReadBlock(tempNode, blockIndex, out byte[] blockData); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "ReadLink: ReadBlock failed for block {0}: {1}", blockIndex, errno); + + return errno; + } + + int toCopy = Math.Min(blockData.Length, (int)(size - bytesRead)); + Array.Copy(blockData, 0, linkData, bytesRead, toCopy); + bytesRead += toCopy; + } + + dest = _encoding.GetString(linkData, 0, (int)size).TrimEnd('\0'); + + AaruLogging.Debug(MODULE_NAME, "ReadLink: target='{0}'", dest); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read) + { + read = 0; + + if(!_mounted) return ErrorNumber.AccessDenied; + + if(node is not CramFileNode 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 block we need + var blockIndex = (int)(currentOffset / PAGE_SIZE); + + // Read and decompress this block + ErrorNumber errno = ReadBlock(fileNode, blockIndex, out byte[] blockData); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "ReadFile: ReadBlock failed for block {0}: {1}", blockIndex, errno); + + if(bytesRead > 0) break; + + return errno; + } + + // Calculate offset within the block + var offsetInBlock = (int)(currentOffset % PAGE_SIZE); + + // Calculate how much to copy from this block + int toCopy = Math.Min(blockData.Length - offsetInBlock, (int)(toRead - bytesRead)); + + if(toCopy <= 0) break; + + Array.Copy(blockData, offsetInBlock, buffer, bytesRead, toCopy); + + bytesRead += toCopy; + currentOffset += toCopy; + } + + read = bytesRead; + fileNode.Offset += bytesRead; + + return ErrorNumber.NoError; + } + + + /// Looks up a file by path and returns its directory entry + /// Path to the file + /// The directory entry info + /// Error number indicating success or failure + ErrorNumber LookupFile(string path, out DirectoryEntryInfo entry) + { + entry = null; + + // Normalize path + string normalizedPath = path ?? "/"; + + if(normalizedPath is "" or ".") normalizedPath = "/"; + + if(normalizedPath == "/") return ErrorNumber.InvalidArgument; + + // Remove leading slash + 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 currentEntries = _rootDirectoryCache; + + // Traverse path to find the file + for(var p = 0; p < pathComponents.Length; p++) + { + string component = pathComponents[p]; + + // Skip "." and ".." + if(component is "." or "..") continue; + + // Find the component in current directory + if(!currentEntries.TryGetValue(component, out DirectoryEntryInfo foundEntry)) return ErrorNumber.NoSuchFile; + + // If this is the last component, return it + if(p == pathComponents.Length - 1) + { + entry = foundEntry; + + return ErrorNumber.NoError; + } + + // Not the last component - must be a directory + if(!IsDirectory(foundEntry.Inode.Mode)) return ErrorNumber.NotDirectory; + + // Read directory contents for next iteration + uint dirOffset = foundEntry.Inode.Offset << 2; + uint dirSize = foundEntry.Inode.Size; + + var dirEntries = new Dictionary(StringComparer.Ordinal); + + ErrorNumber errno = ReadDirectoryContents(dirOffset, dirSize, dirEntries); + + if(errno != ErrorNumber.NoError) return errno; + + currentEntries = dirEntries; + } + + return ErrorNumber.NoSuchFile; + } } \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Internal.cs b/Aaru.Filesystems/Cram/Internal.cs index 6b299ecb8..1819869d8 100644 --- a/Aaru.Filesystems/Cram/Internal.cs +++ b/Aaru.Filesystems/Cram/Internal.cs @@ -66,17 +66,21 @@ public sealed partial class Cram sealed class CramFileNode : IFileNode { /// The cramfs inode - internal Inode Inode { get; init; } + internal Inode Inode { get; set; } + + /// Byte offset of block pointers in the filesystem (inode.Offset << 2) + internal uint BlockPtrOffset { get; set; } + + /// Number of blocks in the file (ceiling of size / PAGE_SIZE) + internal uint BlockCount { get; set; } - /// Byte offset of data in the filesystem - internal uint DataOffset { get; init; } /// - public string Path { get; init; } + public string Path { get; set; } /// Current read position within the file public long Offset { get; set; } /// File length in bytes - public long Length { get; init; } + public long Length { get; set; } } } \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Unimplemented.cs b/Aaru.Filesystems/Cram/Unimplemented.cs deleted file mode 100644 index d6526877c..000000000 --- a/Aaru.Filesystems/Cram/Unimplemented.cs +++ /dev/null @@ -1,54 +0,0 @@ -// /*************************************************************************** -// Aaru Data Preservation Suite -// ---------------------------------------------------------------------------- -// -// Filename : Unimplemented.cs -// Author(s) : Natalia Portillo -// -// Component : Cram file system 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 . -// -// ---------------------------------------------------------------------------- -// Copyright © 2011-2026 Natalia Portillo -// ****************************************************************************/ - -// ReSharper disable UnusedMember.Local - -using System; -using System.Diagnostics.CodeAnalysis; -using Aaru.CommonTypes.Enums; -using Aaru.CommonTypes.Interfaces; - -namespace Aaru.Filesystems; - -/// -[SuppressMessage("ReSharper", "UnusedType.Local")] -public sealed partial class Cram -{ - /// - public ErrorNumber ReadLink(string path, out string dest) => throw new NotImplementedException(); - - /// - public ErrorNumber OpenFile(string path, out IFileNode node) => throw new NotImplementedException(); - - /// - public ErrorNumber CloseFile(IFileNode node) => throw new NotImplementedException(); - - /// - public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read) => - throw new NotImplementedException(); -} \ No newline at end of file