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