[cramfs] Implement mount.

This commit is contained in:
2026-02-08 13:45:00 +00:00
parent c7fdb79b81
commit ef49b119aa
6 changed files with 557 additions and 10 deletions

View File

@@ -0,0 +1,72 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Block.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using Aaru.CommonTypes.Enums;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class Cram
{
/// <summary>Reads bytes from the filesystem</summary>
/// <param name="offset">Byte offset from start of filesystem</param>
/// <param name="length">Number of bytes to read</param>
/// <param name="data">Output data buffer</param>
/// <returns>Error number indicating success or failure</returns>
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;
}
}

View File

@@ -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";
/// <summary>Cached root directory entries</summary>
Dictionary<string, DirectoryEntryInfo> _rootDirectoryCache;
/// <summary>Encoding used for filenames</summary>
Encoding _encoding;
/// <summary>Image plugin being accessed</summary>
IMediaImage _imagePlugin;
/// <summary>Whether filesystem is mounted</summary>
bool _mounted;
/// <summary>Partition being mounted</summary>
Partition _partition;
/// <summary>The superblock</summary>
SuperBlock _superBlock;
/// <summary>Whether the filesystem is little-endian</summary>
bool _littleEndian;
/// <summary>Base offset for the filesystem (0 or 512 for shifted superblock)</summary>
uint _baseOffset;
#region IFilesystem Members
/// <inheritdoc />

View File

@@ -0,0 +1,125 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Dir.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using Aaru.CommonTypes.Enums;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class Cram
{
/// <summary>Reads and parses directory contents</summary>
/// <param name="offset">Byte offset of the directory data</param>
/// <param name="size">Size of the directory in bytes</param>
/// <param name="entries">Dictionary to populate with entries</param>
/// <returns>Error number indicating success or failure</returns>
ErrorNumber ReadDirectoryContents(uint offset, uint size, Dictionary<string, DirectoryEntryInfo> 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<Inode>(inodeData)
: Marshal.ByteArrayToStructureBigEndian<Inode>(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;
}
/// <summary>Checks if a mode indicates a directory</summary>
static bool IsDirectory(ushort mode) => (mode & 0xF000) == 0x4000; // S_IFDIR
}

View File

@@ -0,0 +1,82 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Internal.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Collections.Generic;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class Cram
{
/// <summary>Internal representation of a directory entry</summary>
sealed class DirectoryEntryInfo
{
/// <summary>Filename</summary>
public string Name { get; set; }
/// <summary>The cramfs inode</summary>
public Inode Inode { get; set; }
/// <summary>Byte offset of this entry in the filesystem</summary>
public uint Offset { get; set; }
}
/// <summary>Directory node for traversing directories</summary>
sealed class CramDirNode : IDirNode
{
/// <summary>Current position in the directory listing</summary>
public int Position { get; set; }
/// <summary>Cached directory entries</summary>
public Dictionary<string, DirectoryEntryInfo> Entries { get; set; }
/// <summary>Entry names for enumeration</summary>
public string[] EntryNames { get; set; }
/// <inheritdoc />
public string Path { get; set; }
}
/// <summary>File node for reading file contents</summary>
sealed class CramFileNode : IFileNode
{
/// <summary>The cramfs inode</summary>
internal Inode Inode { get; init; }
/// <summary>Byte offset of data in the filesystem</summary>
internal uint DataOffset { get; init; }
/// <inheritdoc />
public string Path { get; init; }
/// <summary>Current read position within the file</summary>
public long Offset { get; set; }
/// <summary>File length in bytes</summary>
public long Length { get; init; }
}
}

View File

@@ -0,0 +1,250 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Mount.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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;
/// <inheritdoc />
public sealed partial class Cram
{
/// <inheritdoc />
public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
Dictionary<string, string> options, string @namespace)
{
AaruLogging.Debug(MODULE_NAME, "Mounting CramFS volume");
_imagePlugin = imagePlugin;
_partition = partition;
_encoding = encoding ?? Encoding.GetEncoding("iso-8859-15");
_rootDirectoryCache = new Dictionary<string, DirectoryEntryInfo>(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;
}
/// <inheritdoc />
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;
}
/// <summary>Reads and validates the CramFS superblock</summary>
/// <returns>Error number indicating success or failure</returns>
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<SuperBlock>(sector);
_littleEndian = true;
}
else if(magic == CRAM_CIGAM)
{
_superBlock = Marshal.ByteArrayToStructureBigEndian<SuperBlock>(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<SuperBlock>(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<SuperBlock>(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;
}
/// <summary>Loads and caches the root directory contents</summary>
/// <returns>Error number indicating success or failure</returns>
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);
}
}

View File

@@ -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
{
/// <inheritdoc />
public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
Dictionary<string, string> options, string @namespace) =>
throw new NotImplementedException();
/// <inheritdoc />
public ErrorNumber Unmount() => throw new NotImplementedException();
/// <inheritdoc />
public ErrorNumber ListXAttr(string path, out List<string> xattrs) => throw new NotImplementedException();