[cramfs] Implement readlink.

This commit is contained in:
2026-02-08 14:49:26 +00:00
parent 375f977edb
commit 766b466dfa
5 changed files with 503 additions and 72 deletions

View File

@@ -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;
}
/// <summary>Reads and decompresses a single block from a file</summary>
/// <param name="fileNode">The file node</param>
/// <param name="blockIndex">Block index (0-based)</param>
/// <param name="blockData">Output decompressed block data</param>
/// <returns>Error number indicating success or failure</returns>
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;
}
}
}

View File

@@ -97,5 +97,43 @@ public sealed partial class Cram
/// </summary>
const int CRAMFS_BLK_DIRECT_PTR_SHIFT = 2;
#endregion
#region POSIX file type constants
/// <summary>File type mask</summary>
const ushort S_IFMT = 0xF000;
/// <summary>Socket</summary>
const ushort S_IFSOCK = 0xC000;
/// <summary>Symbolic link</summary>
const ushort S_IFLNK = 0xA000;
/// <summary>Regular file</summary>
const ushort S_IFREG = 0x8000;
/// <summary>Block device</summary>
const ushort S_IFBLK = 0x6000;
/// <summary>Directory</summary>
const ushort S_IFDIR = 0x4000;
/// <summary>Character device</summary>
const ushort S_IFCHR = 0x2000;
/// <summary>FIFO (named pipe)</summary>
const ushort S_IFIFO = 0x1000;
/// <summary>Permission bits mask</summary>
const ushort S_IPERM = 0x0FFF;
#endregion
#region Page size
/// <summary>CramFS page size (4KB)</summary>
const int PAGE_SIZE = 4096;
#endregion
}

View File

@@ -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;
/// <inheritdoc />
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
/// <inheritdoc />
public ErrorNumber Stat(string path, out FileEntryInfo stat)
{
@@ -136,4 +125,277 @@ public sealed partial class Cram
return ErrorNumber.NoSuchFile;
}
/// <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);
// 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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <summary>Looks up a file by path and returns its directory entry</summary>
/// <param name="path">Path to the file</param>
/// <param name="entry">The directory entry info</param>
/// <returns>Error number indicating success or failure</returns>
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<string, DirectoryEntryInfo> 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<string, DirectoryEntryInfo>(StringComparer.Ordinal);
ErrorNumber errno = ReadDirectoryContents(dirOffset, dirSize, dirEntries);
if(errno != ErrorNumber.NoError) return errno;
currentEntries = dirEntries;
}
return ErrorNumber.NoSuchFile;
}
}

View File

@@ -66,17 +66,21 @@ public sealed partial class Cram
sealed class CramFileNode : IFileNode
{
/// <summary>The cramfs inode</summary>
internal Inode Inode { get; init; }
internal Inode Inode { get; set; }
/// <summary>Byte offset of block pointers in the filesystem (inode.Offset &lt;&lt; 2)</summary>
internal uint BlockPtrOffset { get; set; }
/// <summary>Number of blocks in the file (ceiling of size / PAGE_SIZE)</summary>
internal uint BlockCount { get; set; }
/// <summary>Byte offset of data in the filesystem</summary>
internal uint DataOffset { get; init; }
/// <inheritdoc />
public string Path { get; init; }
public string Path { get; set; }
/// <summary>Current read position within the file</summary>
public long Offset { get; set; }
/// <summary>File length in bytes</summary>
public long Length { get; init; }
public long Length { get; set; }
}
}

View File

@@ -1,54 +0,0 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Unimplemented.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
// ****************************************************************************/
// ReSharper disable UnusedMember.Local
using System;
using System.Diagnostics.CodeAnalysis;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Filesystems;
/// <inheritdoc />
[SuppressMessage("ReSharper", "UnusedType.Local")]
public sealed partial class Cram
{
/// <inheritdoc />
public ErrorNumber ReadLink(string path, out string dest) => throw new NotImplementedException();
/// <inheritdoc />
public ErrorNumber OpenFile(string path, out IFileNode node) => throw new NotImplementedException();
/// <inheritdoc />
public ErrorNumber CloseFile(IFileNode node) => throw new NotImplementedException();
/// <inheritdoc />
public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read) =>
throw new NotImplementedException();
}