From ef49b119aad10ab46cbbdcfffdfed50361ffc953 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sun, 8 Feb 2026 13:45:00 +0000 Subject: [PATCH] [cramfs] Implement mount. --- Aaru.Filesystems/Cram/Block.cs | 72 +++++++ Aaru.Filesystems/Cram/Cram.cs | 28 +++ Aaru.Filesystems/Cram/Dir.cs | 125 +++++++++++++ Aaru.Filesystems/Cram/Internal.cs | 82 ++++++++ Aaru.Filesystems/Cram/Mount.cs | 250 +++++++++++++++++++++++++ Aaru.Filesystems/Cram/Unimplemented.cs | 10 - 6 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 Aaru.Filesystems/Cram/Block.cs create mode 100644 Aaru.Filesystems/Cram/Dir.cs create mode 100644 Aaru.Filesystems/Cram/Internal.cs create mode 100644 Aaru.Filesystems/Cram/Mount.cs diff --git a/Aaru.Filesystems/Cram/Block.cs b/Aaru.Filesystems/Cram/Block.cs new file mode 100644 index 000000000..a3e6c6f5a --- /dev/null +++ b/Aaru.Filesystems/Cram/Block.cs @@ -0,0 +1,72 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Block.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 +// ****************************************************************************/ + +using System; +using Aaru.CommonTypes.Enums; + +namespace Aaru.Filesystems; + +/// +public sealed partial class Cram +{ + /// Reads bytes from the filesystem + /// Byte offset from start of filesystem + /// Number of bytes to read + /// Output data buffer + /// Error number indicating success or failure + ErrorNumber ReadBytes(uint offset, uint length, out byte[] data) + { + data = null; + + // Add base offset + uint actualOffset = _baseOffset + offset; + + // Calculate sector and offset + uint sectorSize = _imagePlugin.Info.SectorSize; + uint startSector = actualOffset / sectorSize; + var offsetInSector = (int)(actualOffset % sectorSize); + var sectorsToRead = (uint)((offsetInSector + length + sectorSize - 1) / sectorSize); + + ErrorNumber errno = _imagePlugin.ReadSectors(_partition.Start + startSector, + false, + sectorsToRead, + out byte[] sectorData, + out _); + + if(errno != ErrorNumber.NoError) return errno; + + data = new byte[length]; + + if(offsetInSector + length <= sectorData.Length) + Array.Copy(sectorData, offsetInSector, data, 0, length); + else + Array.Copy(sectorData, offsetInSector, data, 0, sectorData.Length - offsetInSector); + + return ErrorNumber.NoError; + } +} \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Cram.cs b/Aaru.Filesystems/Cram/Cram.cs index 487804d4a..7c8e4cb72 100644 --- a/Aaru.Filesystems/Cram/Cram.cs +++ b/Aaru.Filesystems/Cram/Cram.cs @@ -31,8 +31,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; using Aaru.CommonTypes.AaruMetadata; using Aaru.CommonTypes.Interfaces; +using Partition = Aaru.CommonTypes.Partition; namespace Aaru.Filesystems; @@ -41,6 +43,32 @@ namespace Aaru.Filesystems; [SuppressMessage("ReSharper", "UnusedType.Local")] public sealed partial class Cram : IReadOnlyFilesystem { + const string MODULE_NAME = "CramFS plugin"; + + /// Cached root directory entries + Dictionary _rootDirectoryCache; + + /// Encoding used for filenames + Encoding _encoding; + + /// Image plugin being accessed + IMediaImage _imagePlugin; + + /// Whether filesystem is mounted + bool _mounted; + + /// Partition being mounted + Partition _partition; + + /// The superblock + SuperBlock _superBlock; + + /// Whether the filesystem is little-endian + bool _littleEndian; + + /// Base offset for the filesystem (0 or 512 for shifted superblock) + uint _baseOffset; + #region IFilesystem Members /// diff --git a/Aaru.Filesystems/Cram/Dir.cs b/Aaru.Filesystems/Cram/Dir.cs new file mode 100644 index 000000000..173139c48 --- /dev/null +++ b/Aaru.Filesystems/Cram/Dir.cs @@ -0,0 +1,125 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Dir.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 +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.Helpers; +using Aaru.Logging; + +namespace Aaru.Filesystems; + +/// +public sealed partial class Cram +{ + /// Reads and parses directory contents + /// Byte offset of the directory data + /// Size of the directory in bytes + /// Dictionary to populate with entries + /// Error number indicating success or failure + ErrorNumber ReadDirectoryContents(uint offset, uint size, Dictionary entries) + { + if(size == 0) return ErrorNumber.NoError; + + // Read the directory data + ErrorNumber errno = ReadBytes(offset, size, out byte[] dirData); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "Error reading directory data at offset {0}: {1}", offset, errno); + + return errno; + } + + // Parse directory entries + // Each entry is: cramfs_inode (12 bytes) + name (padded to 4 bytes) + uint currentOffset = 0; + + while(currentOffset < size) + { + if(currentOffset + 12 > dirData.Length) break; + + var inodeData = new byte[12]; + Array.Copy(dirData, currentOffset, inodeData, 0, 12); + + Inode inode = _littleEndian + ? Marshal.ByteArrayToStructureLittleEndian(inodeData) + : Marshal.ByteArrayToStructureBigEndian(inodeData); + + // Name length is stored as (actual_length + 3) / 4, so multiply by 4 to get padded length + int nameLen = inode.NameLen << 2; + + if(nameLen == 0) + { + AaruLogging.Debug(MODULE_NAME, "Zero name length at offset {0}", currentOffset); + + break; + } + + if(currentOffset + 12 + nameLen > dirData.Length) + { + AaruLogging.Debug(MODULE_NAME, "Name extends beyond directory data"); + + break; + } + + // Read the name (it's null-padded to 4-byte boundary) + var nameBytes = new byte[nameLen]; + Array.Copy(dirData, currentOffset + 12, nameBytes, 0, nameLen); + + string name = StringHandlers.CToString(nameBytes, _encoding); + + if(string.IsNullOrEmpty(name)) + { + AaruLogging.Debug(MODULE_NAME, "Empty name at offset {0}", currentOffset); + + break; + } + + // Skip . and .. entries + if(name is not "." and not "..") + { + var entry = new DirectoryEntryInfo + { + Name = name, + Inode = inode, + Offset = offset + currentOffset + }; + + entries.TryAdd(name, entry); + } + + currentOffset += (uint)(12 + nameLen); + } + + return ErrorNumber.NoError; + } + + /// Checks if a mode indicates a directory + static bool IsDirectory(ushort mode) => (mode & 0xF000) == 0x4000; // S_IFDIR +} \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Internal.cs b/Aaru.Filesystems/Cram/Internal.cs new file mode 100644 index 000000000..6b299ecb8 --- /dev/null +++ b/Aaru.Filesystems/Cram/Internal.cs @@ -0,0 +1,82 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Internal.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 +// ****************************************************************************/ + +using System.Collections.Generic; +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Filesystems; + +/// +public sealed partial class Cram +{ + /// Internal representation of a directory entry + sealed class DirectoryEntryInfo + { + /// Filename + public string Name { get; set; } + + /// The cramfs inode + public Inode Inode { get; set; } + + /// Byte offset of this entry in the filesystem + public uint Offset { get; set; } + } + + /// Directory node for traversing directories + sealed class CramDirNode : IDirNode + { + /// Current position in the directory listing + public int Position { get; set; } + + /// Cached directory entries + public Dictionary Entries { get; set; } + + /// Entry names for enumeration + public string[] EntryNames { get; set; } + /// + public string Path { get; set; } + } + + /// File node for reading file contents + sealed class CramFileNode : IFileNode + { + /// The cramfs inode + internal Inode Inode { get; init; } + + /// Byte offset of data in the filesystem + internal uint DataOffset { get; init; } + /// + public string Path { get; init; } + + /// Current read position within the file + public long Offset { get; set; } + + /// File length in bytes + public long Length { get; init; } + } +} \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Mount.cs b/Aaru.Filesystems/Cram/Mount.cs new file mode 100644 index 000000000..64bc5612f --- /dev/null +++ b/Aaru.Filesystems/Cram/Mount.cs @@ -0,0 +1,250 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Mount.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 +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Text; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; +using Aaru.Logging; +using Partition = Aaru.CommonTypes.Partition; + +namespace Aaru.Filesystems; + +/// +public sealed partial class Cram +{ + /// + public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, + Dictionary options, string @namespace) + { + AaruLogging.Debug(MODULE_NAME, "Mounting CramFS volume"); + + _imagePlugin = imagePlugin; + _partition = partition; + _encoding = encoding ?? Encoding.GetEncoding("iso-8859-15"); + _rootDirectoryCache = new Dictionary(StringComparer.Ordinal); + + // Read and validate the superblock + ErrorNumber errno = ReadSuperBlock(); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "Error reading superblock: {0}", errno); + + return errno; + } + + AaruLogging.Debug(MODULE_NAME, "Superblock read successfully"); + + // Check for unsupported flags + var flags = (CramFlags)_superBlock.flags; + + if(((uint)flags & ~(uint)CramSupportedFlags.All) != 0) + { + AaruLogging.Debug(MODULE_NAME, "Unsupported flags: 0x{0:X8}", _superBlock.flags); + + return ErrorNumber.NotSupported; + } + + // Validate root inode is a directory + if(!IsDirectory(_superBlock.root.Mode)) + { + AaruLogging.Debug(MODULE_NAME, "Root is not a directory, mode: 0x{0:X4}", _superBlock.root.Mode); + + return ErrorNumber.InvalidArgument; + } + + // Load root directory + errno = LoadRootDirectory(); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "Error loading root directory: {0}", errno); + + return errno; + } + + AaruLogging.Debug(MODULE_NAME, "Root directory loaded with {0} entries", _rootDirectoryCache.Count); + + // Build metadata + string volumeName = StringHandlers.CToString(_superBlock.name, _encoding); + + Metadata = new FileSystem + { + Type = FS_TYPE, + VolumeName = volumeName, + ClusterSize = 4096, // CramFS uses 4K pages + Clusters = _superBlock.fsid.blocks, + Files = _superBlock.fsid.files, + FreeClusters = 0 // CramFS is read-only, no free space + }; + + _mounted = true; + + AaruLogging.Debug(MODULE_NAME, "Volume mounted successfully"); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber Unmount() + { + if(!_mounted) return ErrorNumber.AccessDenied; + + AaruLogging.Debug(MODULE_NAME, "Unmounting volume"); + + _rootDirectoryCache?.Clear(); + _mounted = false; + _imagePlugin = null; + _partition = default(Partition); + _superBlock = default(SuperBlock); + _encoding = null; + _baseOffset = 0; + Metadata = null; + + AaruLogging.Debug(MODULE_NAME, "Volume unmounted"); + + return ErrorNumber.NoError; + } + + /// Reads and validates the CramFS superblock + /// Error number indicating success or failure + ErrorNumber ReadSuperBlock() + { + AaruLogging.Debug(MODULE_NAME, "Reading superblock..."); + + // Read the first sector + ErrorNumber errno = _imagePlugin.ReadSector(_partition.Start, false, out byte[] sector, out _); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "Error reading first sector: {0}", errno); + + return errno; + } + + // Check magic at offset 0 + var magic = BitConverter.ToUInt32(sector, 0); + + _baseOffset = 0; + + if(magic == CRAM_MAGIC) + { + _superBlock = Marshal.ByteArrayToStructureLittleEndian(sector); + _littleEndian = true; + } + else if(magic == CRAM_CIGAM) + { + _superBlock = Marshal.ByteArrayToStructureBigEndian(sector); + _littleEndian = false; + } + else + { + // Check at 512 byte offset (some cramfs images have the superblock shifted) + if(sector.Length >= 512 + 4) + { + magic = BitConverter.ToUInt32(sector, 512); + + if(magic == CRAM_MAGIC) + { + var sbData = new byte[sector.Length - 512]; + Array.Copy(sector, 512, sbData, 0, sbData.Length); + _superBlock = Marshal.ByteArrayToStructureLittleEndian(sbData); + _littleEndian = true; + _baseOffset = 512; + } + else if(magic == CRAM_CIGAM) + { + var sbData = new byte[sector.Length - 512]; + Array.Copy(sector, 512, sbData, 0, sbData.Length); + _superBlock = Marshal.ByteArrayToStructureBigEndian(sbData); + _littleEndian = false; + _baseOffset = 512; + } + else + { + AaruLogging.Debug(MODULE_NAME, "Invalid magic: 0x{0:X8}", magic); + + return ErrorNumber.InvalidArgument; + } + } + else + { + AaruLogging.Debug(MODULE_NAME, "Invalid magic: 0x{0:X8}", magic); + + return ErrorNumber.InvalidArgument; + } + } + + AaruLogging.Debug(MODULE_NAME, + "Magic: 0x{0:X8}, little-endian: {1}, base offset: {2}", + _superBlock.magic, + _littleEndian, + _baseOffset); + + // Validate signature + string signature = Encoding.ASCII.GetString(_superBlock.signature); + + if(!signature.StartsWith(CRAMFS_SIGNATURE, StringComparison.Ordinal)) + { + AaruLogging.Debug(MODULE_NAME, "Invalid signature: {0}", signature); + + return ErrorNumber.InvalidArgument; + } + + AaruLogging.Debug(MODULE_NAME, "Filesystem size: {0} bytes", _superBlock.size); + AaruLogging.Debug(MODULE_NAME, "Flags: 0x{0:X8}", _superBlock.flags); + AaruLogging.Debug(MODULE_NAME, "Files: {0}, Blocks: {1}", _superBlock.fsid.files, _superBlock.fsid.blocks); + + return ErrorNumber.NoError; + } + + /// Loads and caches the root directory contents + /// Error number indicating success or failure + ErrorNumber LoadRootDirectory() + { + // Root directory offset is in the root inode + uint rootOffset = _superBlock.root.Offset << 2; + uint rootSize = _superBlock.root.Size; + + AaruLogging.Debug(MODULE_NAME, "Root directory offset: {0}, size: {1}", rootOffset, rootSize); + + if(rootOffset == 0) + { + // Empty filesystem + AaruLogging.Debug(MODULE_NAME, "Empty filesystem (root offset is 0)"); + + return ErrorNumber.NoError; + } + + return ReadDirectoryContents(rootOffset, rootSize, _rootDirectoryCache); + } +} \ No newline at end of file diff --git a/Aaru.Filesystems/Cram/Unimplemented.cs b/Aaru.Filesystems/Cram/Unimplemented.cs index 1865a4bcf..16da0d59d 100644 --- a/Aaru.Filesystems/Cram/Unimplemented.cs +++ b/Aaru.Filesystems/Cram/Unimplemented.cs @@ -31,8 +31,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text; -using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; @@ -43,14 +41,6 @@ namespace Aaru.Filesystems; [SuppressMessage("ReSharper", "UnusedType.Local")] public sealed partial class Cram { - /// - public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, - Dictionary options, string @namespace) => - throw new NotImplementedException(); - - /// - public ErrorNumber Unmount() => throw new NotImplementedException(); - /// public ErrorNumber ListXAttr(string path, out List xattrs) => throw new NotImplementedException();