mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
504 lines
19 KiB
C#
504 lines
19 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : Block.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Linux extended filesystem 2, 3 and 4 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.IO;
|
|
using System.IO.Compression;
|
|
using Aaru.CommonTypes.Enums;
|
|
using Aaru.Compression;
|
|
using Aaru.Logging;
|
|
using Marshal = Aaru.Helpers.Marshal;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
public sealed partial class ext2FS
|
|
{
|
|
/// <summary>Gets data blocks via direct/indirect block pointers</summary>
|
|
/// <param name="inode">The inode with traditional block pointers</param>
|
|
/// <param name="blockList">List to add blocks to</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber GetIndirectBlocks(Inode inode, List<(ulong physicalBlock, uint length)> blockList)
|
|
{
|
|
ulong fileSize = (ulong)inode.size_high << 32 | inode.size_lo;
|
|
var blocksUsed = (uint)((fileSize + _blockSize - 1) / _blockSize);
|
|
|
|
uint addrsPerBlock = _blockSize / 4;
|
|
uint blockIndex = 0;
|
|
|
|
// Direct blocks (0-11)
|
|
for(uint i = 0; i < 12 && blockIndex < blocksUsed; i++, blockIndex++)
|
|
{
|
|
if(inode.block[i] != 0) blockList.Add((inode.block[i], 1));
|
|
}
|
|
|
|
// Single indirect (block[12])
|
|
if(blockIndex < blocksUsed && inode.block[12] != 0)
|
|
{
|
|
ErrorNumber errno =
|
|
ReadIndirectBlock(inode.block[12], addrsPerBlock, blocksUsed, ref blockIndex, blockList);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
}
|
|
|
|
// Double indirect (block[13])
|
|
if(blockIndex < blocksUsed && inode.block[13] != 0)
|
|
{
|
|
ErrorNumber errno = ReadBytes(inode.block[13] * _blockSize, _blockSize, out byte[] dindData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
for(uint i = 0; i < addrsPerBlock && blockIndex < blocksUsed; i++)
|
|
{
|
|
var indBlock = BitConverter.ToUInt32(dindData, (int)(i * 4));
|
|
|
|
if(indBlock == 0)
|
|
{
|
|
blockIndex += addrsPerBlock;
|
|
|
|
continue;
|
|
}
|
|
|
|
errno = ReadIndirectBlock(indBlock, addrsPerBlock, blocksUsed, ref blockIndex, blockList);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
}
|
|
}
|
|
|
|
// Triple indirect (block[14])
|
|
if(blockIndex < blocksUsed && inode.block[14] != 0)
|
|
{
|
|
ErrorNumber errno = ReadBytes(inode.block[14] * _blockSize, _blockSize, out byte[] tindData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
for(uint i = 0; i < addrsPerBlock && blockIndex < blocksUsed; i++)
|
|
{
|
|
var dindBlock = BitConverter.ToUInt32(tindData, (int)(i * 4));
|
|
|
|
if(dindBlock == 0)
|
|
{
|
|
blockIndex += addrsPerBlock * addrsPerBlock;
|
|
|
|
continue;
|
|
}
|
|
|
|
errno = ReadBytes(dindBlock * _blockSize, _blockSize, out byte[] dindData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
for(uint j = 0; j < addrsPerBlock && blockIndex < blocksUsed; j++)
|
|
{
|
|
var indBlock = BitConverter.ToUInt32(dindData, (int)(j * 4));
|
|
|
|
if(indBlock == 0)
|
|
{
|
|
blockIndex += addrsPerBlock;
|
|
|
|
continue;
|
|
}
|
|
|
|
errno = ReadIndirectBlock(indBlock, addrsPerBlock, blocksUsed, ref blockIndex, blockList);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads block addresses from a single indirect block</summary>
|
|
/// <param name="indirectBlock">Physical block number of the indirect block</param>
|
|
/// <param name="addrsPerBlock">Number of block addresses per block</param>
|
|
/// <param name="blocksUsed">Total number of blocks used by the file</param>
|
|
/// <param name="blockIndex">Current block index (updated on return)</param>
|
|
/// <param name="blockList">List to add blocks to</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber ReadIndirectBlock(uint indirectBlock, uint addrsPerBlock, uint blocksUsed, ref uint blockIndex,
|
|
List<(ulong physicalBlock, uint length)> blockList)
|
|
{
|
|
ErrorNumber errno = ReadBytes(indirectBlock * _blockSize, _blockSize, out byte[] indData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
for(uint i = 0; i < addrsPerBlock && blockIndex < blocksUsed; i++, blockIndex++)
|
|
{
|
|
var blockAddr = BitConverter.ToUInt32(indData, (int)(i * 4));
|
|
|
|
if(blockAddr != 0) blockList.Add((blockAddr, 1));
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads a specified number of bytes from a byte offset within the partition</summary>
|
|
/// <param name="byteOffset">Byte offset from the start of the partition</param>
|
|
/// <param name="count">Number of bytes to read</param>
|
|
/// <param name="data">The read data</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber ReadBytes(ulong byteOffset, uint count, out byte[] data)
|
|
{
|
|
data = null;
|
|
|
|
uint sectorSize = _imagePlugin.Info.SectorSize;
|
|
ulong sectorAddr = byteOffset / sectorSize;
|
|
var offsetInSect = (uint)(byteOffset % sectorSize);
|
|
|
|
uint sectorsToRead = (count + offsetInSect + sectorSize - 1) / sectorSize;
|
|
|
|
ErrorNumber errno = _imagePlugin.ReadSectors(_partition.Start + sectorAddr,
|
|
false,
|
|
sectorsToRead,
|
|
out byte[] sectorData,
|
|
out _);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
if(offsetInSect + count > sectorData.Length)
|
|
{
|
|
// If we can't read all the requested data, return what we have
|
|
var available = (uint)(sectorData.Length - offsetInSect);
|
|
|
|
if(available == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
data = new byte[available];
|
|
Array.Copy(sectorData, (int)offsetInSect, data, 0, (int)available);
|
|
}
|
|
else
|
|
{
|
|
data = new byte[count];
|
|
Array.Copy(sectorData, (int)offsetInSect, data, 0, (int)count);
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads a logical block of a file from its pre-computed block list</summary>
|
|
/// <param name="blockList">The file's block list (physical block, contiguous length) pairs</param>
|
|
/// <param name="logicalBlock">The logical block index to read</param>
|
|
/// <param name="blockData">The block data</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber ReadLogicalBlock(List<(ulong physicalBlock, uint length)> blockList, ulong logicalBlock,
|
|
out byte[] blockData)
|
|
{
|
|
blockData = null;
|
|
|
|
// Walk the block list to find which entry covers this logical block
|
|
ulong currentLogical = 0;
|
|
|
|
foreach((ulong physicalBlock, uint length) in blockList)
|
|
{
|
|
if(logicalBlock < currentLogical + length)
|
|
{
|
|
// Found it — compute the exact physical block
|
|
ulong offsetInExtent = logicalBlock - currentLogical;
|
|
ulong targetPhysical = physicalBlock + offsetInExtent;
|
|
|
|
return ReadBytes(targetPhysical * _blockSize, _blockSize, out blockData);
|
|
}
|
|
|
|
currentLogical += length;
|
|
}
|
|
|
|
// Logical block not found in block list — sparse/hole, return empty
|
|
blockData = Array.Empty<byte>();
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads a logical block from a compressed file, decompressing the cluster as needed</summary>
|
|
/// <param name="fileNode">The file node with compression state and cluster cache</param>
|
|
/// <param name="logicalBlock">The logical block index to read</param>
|
|
/// <param name="blockData">The decompressed block data</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber ReadCompressedLogicalBlock(Ext2FileNode fileNode, ulong logicalBlock, out byte[] blockData)
|
|
{
|
|
blockData = null;
|
|
|
|
uint clusterNBlocks = fileNode.ClusterNBlocks;
|
|
var clusterIndex = (long)(logicalBlock / clusterNBlocks);
|
|
var blockInCluster = (int)(logicalBlock % clusterNBlocks);
|
|
var blockOffset = (int)((ulong)blockInCluster * _blockSize);
|
|
|
|
// Check cluster cache
|
|
if(fileNode.DecompressedClusterCache.TryGetValue(clusterIndex, out byte[] clusterData))
|
|
{
|
|
if(blockOffset + (int)_blockSize <= clusterData.Length)
|
|
{
|
|
blockData = new byte[_blockSize];
|
|
Array.Copy(clusterData, blockOffset, blockData, 0, (int)_blockSize);
|
|
}
|
|
else if(blockOffset < clusterData.Length)
|
|
{
|
|
// Partial last block
|
|
int available = clusterData.Length - blockOffset;
|
|
blockData = new byte[_blockSize];
|
|
Array.Copy(clusterData, blockOffset, blockData, 0, available);
|
|
}
|
|
else
|
|
blockData = new byte[_blockSize];
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Read the cluster and decompress it
|
|
ErrorNumber errno = ReadAndDecompressCluster(fileNode, clusterIndex, out clusterData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Cache the decompressed cluster
|
|
fileNode.DecompressedClusterCache[clusterIndex] = clusterData;
|
|
|
|
// Extract the requested block
|
|
if(blockOffset + (int)_blockSize <= clusterData.Length)
|
|
{
|
|
blockData = new byte[_blockSize];
|
|
Array.Copy(clusterData, blockOffset, blockData, 0, (int)_blockSize);
|
|
}
|
|
else if(blockOffset < clusterData.Length)
|
|
{
|
|
int available = clusterData.Length - blockOffset;
|
|
blockData = new byte[_blockSize];
|
|
Array.Copy(clusterData, blockOffset, blockData, 0, available);
|
|
}
|
|
else
|
|
blockData = new byte[_blockSize];
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads all physical blocks of a cluster and decompresses them</summary>
|
|
/// <param name="fileNode">The file node</param>
|
|
/// <param name="clusterIndex">The cluster index within the file</param>
|
|
/// <param name="decompressedData">The decompressed cluster data</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
ErrorNumber ReadAndDecompressCluster(Ext2FileNode fileNode, long clusterIndex, out byte[] decompressedData)
|
|
{
|
|
decompressedData = null;
|
|
|
|
uint clusterNBlocks = fileNode.ClusterNBlocks;
|
|
ulong firstLogicalBlock = (ulong)clusterIndex * clusterNBlocks;
|
|
uint clusterSizeInBytes = clusterNBlocks * _blockSize;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadAndDecompressCluster: cluster={0}, firstBlock={1}, nblocks={2}",
|
|
clusterIndex,
|
|
firstLogicalBlock,
|
|
clusterNBlocks);
|
|
|
|
// Read all blocks of the cluster
|
|
var rawCluster = new byte[clusterSizeInBytes];
|
|
var bytesRead = 0;
|
|
|
|
for(uint i = 0; i < clusterNBlocks; i++)
|
|
{
|
|
ulong logBlock = firstLogicalBlock + i;
|
|
|
|
ErrorNumber errno = ReadLogicalBlock(fileNode.BlockList, logBlock, out byte[] blockData);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadAndDecompressCluster: failed reading block {0}: {1}",
|
|
logBlock,
|
|
errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
if(blockData != null && blockData.Length > 0)
|
|
{
|
|
int toCopy = Math.Min(blockData.Length, (int)_blockSize);
|
|
Array.Copy(blockData, 0, rawCluster, bytesRead, toCopy);
|
|
bytesRead += toCopy;
|
|
}
|
|
else
|
|
bytesRead += (int)_blockSize;
|
|
}
|
|
|
|
// Check for cluster head magic in the first 2 bytes
|
|
var magic = BitConverter.ToUInt16(rawCluster, 0);
|
|
|
|
if(magic != EXT2_COMPRESS_MAGIC_04X)
|
|
{
|
|
// Not compressed — return raw data
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadAndDecompressCluster: cluster {0} not compressed (magic=0x{1:X4})",
|
|
clusterIndex,
|
|
magic);
|
|
|
|
decompressedData = rawCluster;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Parse the cluster head
|
|
int headSize = Marshal.SizeOf<CompressedClusterHead>();
|
|
|
|
CompressedClusterHead head =
|
|
Marshal.ByteArrayToStructureLittleEndian<CompressedClusterHead>(rawCluster, 0, headSize);
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadAndDecompressCluster: method={0}, ulen={1}, clen={2}, holemap_nbytes={3}",
|
|
head.method,
|
|
head.ulen,
|
|
head.clen,
|
|
head.holemap_nbytes);
|
|
|
|
// Calculate offset to the compressed data (after header + holemap)
|
|
int compressedDataOffset = headSize + head.holemap_nbytes;
|
|
|
|
if(compressedDataOffset + (int)head.clen > rawCluster.Length)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "ReadAndDecompressCluster: compressed data extends beyond cluster");
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Extract compressed data
|
|
var compressedData = new byte[head.clen];
|
|
Array.Copy(rawCluster, compressedDataOffset, compressedData, 0, (int)head.clen);
|
|
|
|
// Decompress
|
|
decompressedData = new byte[head.ulen];
|
|
|
|
ErrorNumber decompResult = DecompressData(head.method, compressedData, decompressedData);
|
|
|
|
if(decompResult != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"ReadAndDecompressCluster: decompression failed for method {0}: {1}",
|
|
head.method,
|
|
decompResult);
|
|
|
|
return decompResult;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "ReadAndDecompressCluster: decompressed {0} -> {1} bytes", head.clen, head.ulen);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Decompresses data using the specified e2compr algorithm</summary>
|
|
/// <param name="method">e2compr algorithm id</param>
|
|
/// <param name="compressedData">The compressed data</param>
|
|
/// <param name="decompressedData">Pre-allocated buffer for decompressed output</param>
|
|
/// <returns>Error number indicating success or failure</returns>
|
|
static ErrorNumber DecompressData(byte method, byte[] compressedData, byte[] decompressedData)
|
|
{
|
|
switch(method)
|
|
{
|
|
case EXT2_GZIP_ALG:
|
|
{
|
|
try
|
|
{
|
|
using var ms = new MemoryStream(compressedData);
|
|
using var zlib = new ZLibStream(ms, CompressionMode.Decompress);
|
|
var pos = 0;
|
|
|
|
while(pos < decompressedData.Length)
|
|
{
|
|
int read = zlib.Read(decompressedData, pos, decompressedData.Length - pos);
|
|
|
|
if(read == 0) break;
|
|
|
|
pos += read;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
catch(Exception)
|
|
{
|
|
// The e2compr gzip format uses raw deflate (no zlib header), try that
|
|
try
|
|
{
|
|
using var ms = new MemoryStream(compressedData);
|
|
using var deflate = new DeflateStream(ms, CompressionMode.Decompress);
|
|
var pos = 0;
|
|
|
|
while(pos < decompressedData.Length)
|
|
{
|
|
int read = deflate.Read(decompressedData, pos, decompressedData.Length - pos);
|
|
|
|
if(read == 0) break;
|
|
|
|
pos += read;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
catch(Exception)
|
|
{
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
}
|
|
}
|
|
|
|
case EXT2_BZIP2_ALG:
|
|
{
|
|
int decoded = BZip2.DecodeBuffer(compressedData, decompressedData);
|
|
|
|
return decoded > 0 ? ErrorNumber.NoError : ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
case EXT2_LZO_ALG:
|
|
{
|
|
int decoded = LZO.DecodeBuffer(compressedData, decompressedData, LZO.Algorithm.LZO1X);
|
|
|
|
return decoded > 0 ? ErrorNumber.NoError : ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
case EXT2_NONE_ALG:
|
|
{
|
|
int toCopy = Math.Min(compressedData.Length, decompressedData.Length);
|
|
Array.Copy(compressedData, 0, decompressedData, 0, toCopy);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
case EXT2_LZRW3A_ALG:
|
|
{
|
|
int decoded = LZRW3A.DecodeBuffer(compressedData, decompressedData);
|
|
|
|
return decoded > 0 ? ErrorNumber.NoError : ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
case EXT2_LZV1_ALG:
|
|
{
|
|
int decoded = LZV1.DecodeBuffer(compressedData, decompressedData);
|
|
|
|
return decoded > 0 ? ErrorNumber.NoError : ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
default:
|
|
return ErrorNumber.NotSupported;
|
|
}
|
|
}
|
|
} |