mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
1780 lines
76 KiB
C#
1780 lines
76 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : File.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Microsoft NT 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.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Structs;
|
|
using Aaru.Helpers;
|
|
using Aaru.Logging;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
/// <inheritdoc />
|
|
public sealed partial class NTFS
|
|
{
|
|
/// <inheritdoc />
|
|
public ErrorNumber Stat(string path, out FileEntryInfo stat)
|
|
{
|
|
stat = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize path
|
|
string normalizedPath = string.IsNullOrWhiteSpace(path) ? "/" : path;
|
|
|
|
if(!normalizedPath.StartsWith("/", StringComparison.Ordinal)) normalizedPath = "/" + normalizedPath;
|
|
|
|
uint mftRecordNumber;
|
|
|
|
if(normalizedPath == "/")
|
|
mftRecordNumber = (uint)SystemFileNumber.Root;
|
|
else
|
|
{
|
|
ErrorNumber resolveErrno = ResolvePathToMftRecord(normalizedPath, out mftRecordNumber);
|
|
|
|
if(resolveErrno != ErrorNumber.NoError) return resolveErrno;
|
|
}
|
|
|
|
ErrorNumber errno = ReadMftRecord(mftRecordNumber, out byte[] recordData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
MftRecord header = ParseMftRecordHeader(recordData);
|
|
|
|
if(header.magic != NtfsRecordMagic.File)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "MFT record {0} has invalid magic", mftRecordNumber);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
if(!header.flags.HasFlag(MftRecordFlags.InUse))
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "MFT record {0} is not in use", mftRecordNumber);
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
stat = new FileEntryInfo
|
|
{
|
|
Inode = mftRecordNumber,
|
|
Links = header.link_count,
|
|
BlockSize = _bytesPerCluster,
|
|
Attributes = header.flags.HasFlag(MftRecordFlags.IsDirectory)
|
|
? FileAttributes.Directory
|
|
: FileAttributes.File
|
|
};
|
|
|
|
// Walk attributes (including extension records via $ATTRIBUTE_LIST)
|
|
ErrorNumber findErrno = FindAllAttributes(recordData, header, mftRecordNumber, out List<FoundAttribute> attrs);
|
|
|
|
if(findErrno != ErrorNumber.NoError) return findErrno;
|
|
|
|
FileAttributeFlags ntfsAttributes = 0;
|
|
var foundStdInfo = false;
|
|
var foundFileName = false;
|
|
long dataSize = 0;
|
|
long dataAllocatedSize = 0;
|
|
var foundData = false;
|
|
var isDataEncrypted = false;
|
|
var isDeduplicated = false;
|
|
FileNameAttribute bestFileName = default;
|
|
FileNameNamespace bestNamespace = FileNameNamespace.Dos;
|
|
uint? lxUid = null;
|
|
uint? lxGid = null;
|
|
uint? lxMod = null;
|
|
ulong? lxDev = null;
|
|
DateTime? lxAtime = null;
|
|
DateTime? lxMtime = null;
|
|
DateTime? lxCtime = null;
|
|
|
|
foreach(FoundAttribute attr in attrs)
|
|
{
|
|
var attrType = (AttributeType)BitConverter.ToUInt32(attr.RecordData, attr.Offset);
|
|
var attrLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 4);
|
|
|
|
if(attrLength == 0) continue;
|
|
|
|
byte nonResident = attr.RecordData[attr.Offset + 8];
|
|
|
|
switch(attrType)
|
|
{
|
|
case AttributeType.StandardInformation when nonResident == 0:
|
|
{
|
|
var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14);
|
|
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
|
|
|
|
int valueStart = attr.Offset + valueOffset;
|
|
|
|
if(valueStart + valueLength <= attr.RecordData.Length)
|
|
{
|
|
if(valueLength >= Marshal.SizeOf<StandardInformationV3>())
|
|
{
|
|
StandardInformationV3 stdInfo =
|
|
Marshal.ByteArrayToStructureLittleEndian<StandardInformationV3>(attr.RecordData,
|
|
valueStart,
|
|
Marshal.SizeOf<StandardInformationV3>());
|
|
|
|
ntfsAttributes = stdInfo.file_attributes;
|
|
|
|
SetTimestamps(stat,
|
|
stdInfo.creation_time,
|
|
stdInfo.last_data_change_time,
|
|
stdInfo.last_mft_change_time,
|
|
stdInfo.last_access_time);
|
|
|
|
stat.UID = stdInfo.owner_id;
|
|
}
|
|
else if(valueLength >= (uint)Marshal.SizeOf<StandardInformationV1>())
|
|
{
|
|
StandardInformationV1 stdInfo =
|
|
Marshal.ByteArrayToStructureLittleEndian<StandardInformationV1>(attr.RecordData,
|
|
valueStart,
|
|
Marshal.SizeOf<StandardInformationV1>());
|
|
|
|
ntfsAttributes = stdInfo.file_attributes;
|
|
|
|
SetTimestamps(stat,
|
|
stdInfo.creation_time,
|
|
stdInfo.last_data_change_time,
|
|
stdInfo.last_mft_change_time,
|
|
stdInfo.last_access_time);
|
|
}
|
|
|
|
foundStdInfo = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case AttributeType.FileName when nonResident == 0:
|
|
{
|
|
var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14);
|
|
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
|
|
|
|
int valueStart = attr.Offset + valueOffset;
|
|
|
|
if(valueStart + Marshal.SizeOf<FileNameAttribute>() <= attr.RecordData.Length)
|
|
{
|
|
FileNameAttribute fnAttr =
|
|
Marshal.ByteArrayToStructureLittleEndian<FileNameAttribute>(attr.RecordData,
|
|
valueStart,
|
|
Marshal.SizeOf<FileNameAttribute>());
|
|
|
|
// Prefer Win32 or Win32AndDos over Posix over Dos
|
|
if(!foundFileName ||
|
|
fnAttr.file_name_type is FileNameNamespace.Win32 or FileNameNamespace.Win32AndDos ||
|
|
fnAttr.file_name_type == FileNameNamespace.Posix && bestNamespace == FileNameNamespace.Dos)
|
|
{
|
|
bestFileName = fnAttr;
|
|
bestNamespace = fnAttr.file_name_type;
|
|
foundFileName = true;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case AttributeType.Data:
|
|
{
|
|
// Only process the unnamed (default) $DATA attribute
|
|
byte nameLength = attr.RecordData[attr.Offset + 9];
|
|
|
|
if(nameLength == 0 && !foundData)
|
|
{
|
|
// Check attribute flags for encryption (EFS)
|
|
var dataAttrFlags = (AttributeFlags)BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x0C);
|
|
|
|
if(dataAttrFlags.HasFlag(AttributeFlags.Encrypted)) isDataEncrypted = true;
|
|
|
|
if(nonResident == 0)
|
|
{
|
|
// Resident $DATA
|
|
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
|
|
dataSize = valueLength;
|
|
dataAllocatedSize = valueLength;
|
|
}
|
|
else
|
|
{
|
|
// Non-resident $DATA — use first extent for size info
|
|
NonResidentAttributeRecord nrAttr =
|
|
Marshal.ByteArrayToStructureLittleEndian<NonResidentAttributeRecord>(attr.RecordData,
|
|
attr.Offset,
|
|
Marshal.SizeOf<NonResidentAttributeRecord>());
|
|
|
|
dataSize = (long)nrAttr.data_size;
|
|
dataAllocatedSize = (long)nrAttr.allocated_size;
|
|
}
|
|
|
|
foundData = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case AttributeType.Ea:
|
|
{
|
|
ErrorNumber eaErrno = ReadEaAttributeData(attr.RecordData,
|
|
attr.Offset,
|
|
nonResident,
|
|
out byte[] eaData,
|
|
out int eaLength);
|
|
|
|
if(eaErrno == ErrorNumber.NoError && eaLength > 0)
|
|
{
|
|
ParseWslEas(eaData,
|
|
0,
|
|
eaLength,
|
|
ref lxUid,
|
|
ref lxGid,
|
|
ref lxMod,
|
|
ref lxDev,
|
|
ref lxAtime,
|
|
ref lxMtime,
|
|
ref lxCtime);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set file size from $DATA or $FILE_NAME
|
|
if(foundData)
|
|
{
|
|
stat.Length = dataSize;
|
|
stat.Blocks = (dataAllocatedSize + _bytesPerCluster - 1) / _bytesPerCluster;
|
|
}
|
|
else if(foundFileName)
|
|
{
|
|
stat.Length = (long)bestFileName.data_size;
|
|
stat.Blocks = (long)((bestFileName.allocated_size + _bytesPerCluster - 1) / _bytesPerCluster);
|
|
}
|
|
|
|
// Map NTFS FileAttributeFlags to Aaru FileAttributes
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.ReadOnly)) stat.Attributes |= FileAttributes.ReadOnly;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Hidden)) stat.Attributes |= FileAttributes.Hidden;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.System)) stat.Attributes |= FileAttributes.System;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Archive)) stat.Attributes |= FileAttributes.Archive;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Device)) stat.Attributes |= FileAttributes.Device;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Temporary)) stat.Attributes |= FileAttributes.Temporary;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.SparseFile)) stat.Attributes |= FileAttributes.Sparse;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.ReparsePoint)) stat.Attributes |= FileAttributes.ReparsePoint;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Compressed)) stat.Attributes |= FileAttributes.Compressed;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Offline)) stat.Attributes |= FileAttributes.Offline;
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.Encrypted)) stat.Attributes |= FileAttributes.Encrypted;
|
|
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.NotContentIndexed)) stat.Attributes |= FileAttributes.NotIndexed;
|
|
|
|
// If $DATA attribute is EFS-encrypted, ensure the Encrypted flag is set
|
|
if(isDataEncrypted) stat.Attributes |= FileAttributes.Encrypted;
|
|
|
|
// If file is deduplicated, set Sparse attribute (dedup files are stored as sparse + reparse)
|
|
if(isDeduplicated) stat.Attributes |= FileAttributes.Sparse;
|
|
|
|
// Synthesize POSIX mode from NTFS attributes
|
|
// NTFS doesn't store POSIX mode natively, but we can approximate:
|
|
// directories get 0755, files get 0644, read-only files get 0444
|
|
uint mode;
|
|
|
|
if(header.flags.HasFlag(MftRecordFlags.IsDirectory))
|
|
{
|
|
// S_IFDIR | rwxr-xr-x
|
|
mode = 0x4000 | 0755;
|
|
}
|
|
else if(ntfsAttributes.HasFlag(FileAttributeFlags.ReparsePoint))
|
|
{
|
|
// Read the reparse tag from $REPARSE_POINT attribute to determine the actual file type
|
|
uint reparseTag = 0;
|
|
|
|
ErrorNumber rpErrno = FindAttributes(recordData,
|
|
header,
|
|
mftRecordNumber,
|
|
AttributeType.ReparsePoint,
|
|
null,
|
|
out List<FoundAttribute> rpResults);
|
|
|
|
if(rpErrno == ErrorNumber.NoError && rpResults.Count > 0)
|
|
{
|
|
FoundAttribute rpAttr = rpResults[0];
|
|
byte rpNonRes = rpAttr.RecordData[rpAttr.Offset + 8];
|
|
|
|
if(rpNonRes == 0)
|
|
{
|
|
var rpValueOffset = BitConverter.ToUInt16(rpAttr.RecordData, rpAttr.Offset + 0x14);
|
|
int rpValueStart = rpAttr.Offset + rpValueOffset;
|
|
|
|
if(rpValueStart + 4 <= rpAttr.RecordData.Length)
|
|
reparseTag = BitConverter.ToUInt32(rpAttr.RecordData, rpValueStart);
|
|
}
|
|
}
|
|
|
|
switch(reparseTag)
|
|
{
|
|
case IO_REPARSE_TAG_LX_FIFO:
|
|
// S_IFIFO | rw-rw-rw-
|
|
mode = 0x1000 | 0666;
|
|
stat.Attributes |= FileAttributes.FIFO;
|
|
|
|
break;
|
|
case IO_REPARSE_TAG_LX_CHR:
|
|
// S_IFCHR | rw-rw-rw-
|
|
mode = 0x2000 | 0666;
|
|
stat.Attributes |= FileAttributes.CharDevice;
|
|
|
|
break;
|
|
case IO_REPARSE_TAG_LX_BLK:
|
|
// S_IFBLK | rw-rw-rw-
|
|
mode = 0x6000 | 0660;
|
|
stat.Attributes |= FileAttributes.BlockDevice;
|
|
|
|
break;
|
|
case IO_REPARSE_TAG_AF_UNIX:
|
|
// S_IFSOCK | rwxrwxrwx
|
|
mode = 0xC000 | 0777;
|
|
stat.Attributes |= FileAttributes.Socket;
|
|
|
|
break;
|
|
case IO_REPARSE_TAG_DEDUP:
|
|
// Deduplicated file — treat as regular file
|
|
isDeduplicated = true;
|
|
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.ReadOnly))
|
|
mode = 0x8000 | 0444; // S_IFREG | r--r--r--
|
|
else
|
|
mode = 0x8000 | 0644; // S_IFREG | rw-r--r--
|
|
|
|
break;
|
|
default:
|
|
// S_IFLNK | rwxrwxrwx (symlinks, mount points, and unknown reparse points)
|
|
mode = 0xA000 | 0777;
|
|
stat.Attributes |= FileAttributes.Symlink;
|
|
|
|
break;
|
|
}
|
|
}
|
|
else if(ntfsAttributes.HasFlag(FileAttributeFlags.Device))
|
|
{
|
|
// S_IFCHR | rw-rw-rw-
|
|
mode = 0x2000 | 0666;
|
|
}
|
|
else
|
|
{
|
|
// S_IFREG
|
|
mode = 0x8000;
|
|
|
|
if(ntfsAttributes.HasFlag(FileAttributeFlags.ReadOnly))
|
|
mode |= 0444; // r--r--r--
|
|
else
|
|
mode |= 0644; // rw-r--r--
|
|
}
|
|
|
|
stat.Mode = mode;
|
|
|
|
// Override with WSL EA values if present
|
|
if(lxUid.HasValue) stat.UID = lxUid.Value;
|
|
|
|
if(lxGid.HasValue) stat.GID = lxGid.Value;
|
|
|
|
if(lxMod.HasValue) stat.Mode = lxMod.Value;
|
|
|
|
if(lxDev.HasValue) stat.DeviceNo = lxDev.Value;
|
|
|
|
if(lxAtime.HasValue) stat.AccessTimeUtc = lxAtime.Value;
|
|
|
|
if(lxMtime.HasValue) stat.LastWriteTimeUtc = lxMtime.Value;
|
|
|
|
if(lxCtime.HasValue) stat.StatusChangeTimeUtc = lxCtime.Value;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber OpenFile(string path, out IFileNode node)
|
|
{
|
|
node = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize path
|
|
string normalizedPath = string.IsNullOrWhiteSpace(path) ? "/" : path;
|
|
|
|
if(!normalizedPath.StartsWith("/", StringComparison.Ordinal)) normalizedPath = "/" + normalizedPath;
|
|
|
|
// Root directory is not a file
|
|
if(normalizedPath == "/") return ErrorNumber.IsDirectory;
|
|
|
|
ErrorNumber resolveErrno = ResolvePathToMftRecord(normalizedPath, out uint mftRecordNumber);
|
|
|
|
if(resolveErrno != ErrorNumber.NoError) return resolveErrno;
|
|
|
|
ErrorNumber errno = ReadMftRecord(mftRecordNumber, out byte[] recordData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
MftRecord header = ParseMftRecordHeader(recordData);
|
|
|
|
if(header.magic != NtfsRecordMagic.File)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "MFT record {0} has invalid magic", mftRecordNumber);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
if(!header.flags.HasFlag(MftRecordFlags.InUse))
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "MFT record {0} is not in use", mftRecordNumber);
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
// Reject directories
|
|
if(header.flags.HasFlag(MftRecordFlags.IsDirectory)) return ErrorNumber.IsDirectory;
|
|
|
|
// Find the unnamed $DATA attribute across base + extension records
|
|
ErrorNumber findErrno = FindAttributes(recordData,
|
|
header,
|
|
mftRecordNumber,
|
|
AttributeType.Data,
|
|
null,
|
|
out List<FoundAttribute> dataAttrs);
|
|
|
|
if(findErrno != ErrorNumber.NoError) return findErrno;
|
|
|
|
if(dataAttrs.Count == 0) return ErrorNumber.NoSuchFile;
|
|
|
|
// Check first extent — if resident, data is inline
|
|
FoundAttribute firstAttr = dataAttrs[0];
|
|
byte nonResident = firstAttr.RecordData[firstAttr.Offset + 8];
|
|
|
|
if(nonResident == 0)
|
|
{
|
|
// Resident $DATA — small file stored in MFT record
|
|
var valueOffset = BitConverter.ToUInt16(firstAttr.RecordData, firstAttr.Offset + 0x14);
|
|
var valueLength = BitConverter.ToUInt32(firstAttr.RecordData, firstAttr.Offset + 0x10);
|
|
|
|
int valueStart = firstAttr.Offset + valueOffset;
|
|
|
|
var residentData = new byte[valueLength];
|
|
|
|
if(valueStart + valueLength <= firstAttr.RecordData.Length)
|
|
Array.Copy(firstAttr.RecordData, valueStart, residentData, 0, valueLength);
|
|
|
|
node = new NtfsFileNode
|
|
{
|
|
Path = normalizedPath,
|
|
Length = valueLength,
|
|
Offset = 0,
|
|
IsResident = true,
|
|
ResidentData = residentData
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Non-resident $DATA — assemble data runs from all extents
|
|
ErrorNumber asmErrno = AssembleNonResidentRuns(mftRecordNumber,
|
|
AttributeType.Data,
|
|
null,
|
|
out List<(long offset, long length)> dataRuns,
|
|
out long dataSize,
|
|
out _,
|
|
out AttributeFlags dataFlags,
|
|
out byte compUnit);
|
|
|
|
if(asmErrno != ErrorNumber.NoError) return asmErrno;
|
|
|
|
bool isCompressed = compUnit != 0 && dataFlags.HasFlag(AttributeFlags.Compressed);
|
|
int compressionUnitClusters = isCompressed ? 1 << compUnit : 0;
|
|
|
|
// Check for WOF (Windows Overlay Filter) external compression via $REPARSE_POINT
|
|
ErrorNumber rpFindErrno = FindAttributes(recordData,
|
|
header,
|
|
mftRecordNumber,
|
|
AttributeType.ReparsePoint,
|
|
null,
|
|
out List<FoundAttribute> rpAttrs);
|
|
|
|
if(rpFindErrno == ErrorNumber.NoError && rpAttrs.Count > 0)
|
|
{
|
|
FoundAttribute rpAttr = rpAttrs[0];
|
|
byte rpNonResident = rpAttr.RecordData[rpAttr.Offset + 8];
|
|
|
|
if(rpNonResident == 0)
|
|
{
|
|
var rpValueOffset = BitConverter.ToUInt16(rpAttr.RecordData, rpAttr.Offset + 0x14);
|
|
var rpValueLength = BitConverter.ToUInt32(rpAttr.RecordData, rpAttr.Offset + 0x10);
|
|
|
|
int rpValueStart = rpAttr.Offset + rpValueOffset;
|
|
int reparseHeaderSize = Marshal.SizeOf<ReparsePointAttribute>();
|
|
|
|
if(rpValueStart + reparseHeaderSize <= rpAttr.RecordData.Length && rpValueLength >= reparseHeaderSize)
|
|
{
|
|
ReparsePointAttribute reparseHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<ReparsePointAttribute>(rpAttr.RecordData,
|
|
rpValueStart,
|
|
reparseHeaderSize);
|
|
|
|
if(reparseHeader.reparse_tag == ReparseTag.Wof)
|
|
{
|
|
// Parse WOF reparse buffer: WofVersion(4) + WofProvider(4) + ProviderVer(4) + Algorithm(4)
|
|
int wofDataStart = rpValueStart + reparseHeaderSize;
|
|
|
|
if(wofDataStart + 16 <= rpAttr.RecordData.Length)
|
|
{
|
|
var wofVersion = BitConverter.ToUInt32(rpAttr.RecordData, wofDataStart);
|
|
var wofProvider = BitConverter.ToUInt32(rpAttr.RecordData, wofDataStart + 4);
|
|
var providerVer = BitConverter.ToUInt32(rpAttr.RecordData, wofDataStart + 8);
|
|
var algorithm = BitConverter.ToUInt32(rpAttr.RecordData, wofDataStart + 12);
|
|
|
|
if(wofVersion == WOF_CURRENT_VERSION &&
|
|
wofProvider == WOF_PROVIDER_SYSTEM &&
|
|
providerVer == WOF_PROVIDER_CURRENT_VERSION)
|
|
{
|
|
int frameSize = algorithm switch
|
|
{
|
|
WOF_COMPRESSION_XPRESS4K => 0x1000, // 4 KiB
|
|
WOF_COMPRESSION_LZX32K => 0x8000, // 32 KiB
|
|
WOF_COMPRESSION_XPRESS8K => 0x2000, // 8 KiB
|
|
WOF_COMPRESSION_XPRESS16K => 0x4000, // 16 KiB
|
|
_ => 0
|
|
};
|
|
|
|
if(frameSize > 0)
|
|
{
|
|
// Read the WofCompressedData named stream
|
|
ErrorNumber wofFindErrno =
|
|
FindAttributes(recordData,
|
|
header,
|
|
mftRecordNumber,
|
|
AttributeType.Data,
|
|
WOF_COMPRESSED_DATA_STREAM,
|
|
out List<FoundAttribute> wofAttrs);
|
|
|
|
if(wofFindErrno == ErrorNumber.NoError && wofAttrs.Count > 0)
|
|
{
|
|
FoundAttribute wofAttr = wofAttrs[0];
|
|
byte wofNonResident = wofAttr.RecordData[wofAttr.Offset + 8];
|
|
|
|
if(wofNonResident == 0)
|
|
{
|
|
// Resident WofCompressedData
|
|
var wofValOff = BitConverter.ToUInt16(wofAttr.RecordData,
|
|
wofAttr.Offset + 0x14);
|
|
|
|
var wofValLen = BitConverter.ToUInt32(wofAttr.RecordData,
|
|
wofAttr.Offset + 0x10);
|
|
|
|
int wofValStart = wofAttr.Offset + wofValOff;
|
|
var wofResident = new byte[wofValLen];
|
|
|
|
if(wofValStart + wofValLen <= wofAttr.RecordData.Length)
|
|
Array.Copy(wofAttr.RecordData, wofValStart, wofResident, 0, wofValLen);
|
|
|
|
node = new NtfsFileNode
|
|
{
|
|
Path = normalizedPath,
|
|
Length = dataSize,
|
|
Offset = 0,
|
|
IsResident = false,
|
|
DataRuns = dataRuns,
|
|
IsWofCompressed = true,
|
|
WofAlgorithm = algorithm,
|
|
WofFrameSize = frameSize,
|
|
WofIsResident = true,
|
|
WofResidentData = wofResident,
|
|
WofDataSize = wofValLen
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Non-resident WofCompressedData
|
|
ErrorNumber wofAsmErrno =
|
|
AssembleNonResidentRuns(mftRecordNumber,
|
|
AttributeType.Data,
|
|
WOF_COMPRESSED_DATA_STREAM,
|
|
out List<(long offset, long length)> wofRuns,
|
|
out long wofSize,
|
|
out _,
|
|
out _,
|
|
out _);
|
|
|
|
if(wofAsmErrno == ErrorNumber.NoError)
|
|
{
|
|
node = new NtfsFileNode
|
|
{
|
|
Path = normalizedPath,
|
|
Length = dataSize,
|
|
Offset = 0,
|
|
IsResident = false,
|
|
DataRuns = dataRuns,
|
|
IsWofCompressed = true,
|
|
WofAlgorithm = algorithm,
|
|
WofFrameSize = frameSize,
|
|
WofDataRuns = wofRuns,
|
|
WofDataSize = wofSize
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
node = new NtfsFileNode
|
|
{
|
|
Path = normalizedPath,
|
|
Length = dataSize,
|
|
Offset = 0,
|
|
IsResident = false,
|
|
DataRuns = dataRuns,
|
|
IsCompressed = isCompressed,
|
|
CompressionUnitClusters = compressionUnitClusters
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseFile(IFileNode node)
|
|
{
|
|
if(node is not NtfsFileNode mynode) return ErrorNumber.InvalidArgument;
|
|
|
|
mynode.DataRuns = null;
|
|
mynode.ResidentData = null;
|
|
mynode.CachedCluster = null;
|
|
mynode.CachedClusterOffset = -1;
|
|
mynode.CachedCompressionUnit = null;
|
|
mynode.CachedCompressionUnitVcn = -1;
|
|
mynode.WofDataRuns = null;
|
|
mynode.WofResidentData = null;
|
|
mynode.CachedWofFrame = null;
|
|
mynode.CachedWofFrameIndex = -1;
|
|
mynode.Offset = -1;
|
|
|
|
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 NtfsFileNode mynode) return ErrorNumber.InvalidArgument;
|
|
|
|
if(mynode.Offset < 0) return ErrorNumber.InvalidArgument;
|
|
|
|
if(length <= 0) return ErrorNumber.NoError;
|
|
|
|
// Clamp to remaining file size
|
|
long remaining = mynode.Length - mynode.Offset;
|
|
|
|
if(remaining <= 0) return ErrorNumber.NoError;
|
|
|
|
if(length > remaining) length = remaining;
|
|
|
|
// Clamp to buffer size
|
|
if(length > buffer.Length) length = buffer.Length;
|
|
|
|
if(mynode.IsResident)
|
|
{
|
|
// Resident data — direct copy
|
|
Array.Copy(mynode.ResidentData, mynode.Offset, buffer, 0, length);
|
|
mynode.Offset += length;
|
|
read = length;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Non-resident data — read from data runs
|
|
if(mynode.IsWofCompressed) return ReadWofFile(mynode, length, buffer, out read);
|
|
|
|
if(mynode.IsCompressed) return ReadCompressedFile(mynode, length, buffer, out read);
|
|
|
|
return ReadUncompressedFile(mynode, length, buffer, out read);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadLink(string path, out string dest)
|
|
{
|
|
dest = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize path
|
|
string normalizedPath = string.IsNullOrWhiteSpace(path) ? "/" : path;
|
|
|
|
if(!normalizedPath.StartsWith("/", StringComparison.Ordinal)) normalizedPath = "/" + normalizedPath;
|
|
|
|
// Root directory cannot be a symlink
|
|
if(normalizedPath == "/") return ErrorNumber.InvalidArgument;
|
|
|
|
ErrorNumber resolveErrno = ResolvePathToMftRecord(normalizedPath, out uint mftRecordNumber);
|
|
|
|
if(resolveErrno != ErrorNumber.NoError) return resolveErrno;
|
|
|
|
ErrorNumber errno = ReadMftRecord(mftRecordNumber, out byte[] recordData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
MftRecord header = ParseMftRecordHeader(recordData);
|
|
|
|
if(header.magic != NtfsRecordMagic.File)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "MFT record {0} has invalid magic", mftRecordNumber);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
if(!header.flags.HasFlag(MftRecordFlags.InUse))
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "MFT record {0} is not in use", mftRecordNumber);
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
// Find $REPARSE_POINT attribute across base + extension records
|
|
ErrorNumber findErrno = FindAttributes(recordData,
|
|
header,
|
|
mftRecordNumber,
|
|
AttributeType.ReparsePoint,
|
|
null,
|
|
out List<FoundAttribute> reparseAttrs);
|
|
|
|
if(findErrno != ErrorNumber.NoError) return findErrno;
|
|
|
|
if(reparseAttrs.Count == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
FoundAttribute reparseAttr = reparseAttrs[0];
|
|
byte nonResident = reparseAttr.RecordData[reparseAttr.Offset + 8];
|
|
|
|
if(nonResident != 0)
|
|
{
|
|
// Reparse points should always be resident
|
|
AaruLogging.Debug(MODULE_NAME, "Non-resident $REPARSE_POINT in MFT record {0}", mftRecordNumber);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
byte[] rd = reparseAttr.RecordData;
|
|
int attrOff = reparseAttr.Offset;
|
|
|
|
var valueOffset = BitConverter.ToUInt16(rd, attrOff + 0x14);
|
|
var valueLength = BitConverter.ToUInt32(rd, attrOff + 0x10);
|
|
|
|
int valueStart = attrOff + valueOffset;
|
|
int reparseHeaderSize = Marshal.SizeOf<ReparsePointAttribute>();
|
|
|
|
if(valueStart + reparseHeaderSize > rd.Length || valueLength < reparseHeaderSize)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
ReparsePointAttribute reparseHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<ReparsePointAttribute>(rd, valueStart, reparseHeaderSize);
|
|
|
|
int dataStart = valueStart + reparseHeaderSize;
|
|
|
|
switch(reparseHeader.reparse_tag)
|
|
{
|
|
case ReparseTag.Symlink:
|
|
{
|
|
if(dataStart + 12 > rd.Length) return ErrorNumber.InvalidArgument;
|
|
|
|
var printNameOffset = BitConverter.ToUInt16(rd, dataStart + 4);
|
|
var printNameLength = BitConverter.ToUInt16(rd, dataStart + 6);
|
|
|
|
int pathBufferStart = dataStart + 12;
|
|
int printNameStart = pathBufferStart + printNameOffset;
|
|
|
|
if(printNameLength > 0 && printNameStart + printNameLength <= rd.Length)
|
|
dest = Encoding.Unicode.GetString(rd, printNameStart, printNameLength);
|
|
else
|
|
{
|
|
var substituteNameOffset = BitConverter.ToUInt16(rd, dataStart);
|
|
var substituteNameLength = BitConverter.ToUInt16(rd, dataStart + 2);
|
|
|
|
int substituteNameStart = pathBufferStart + substituteNameOffset;
|
|
|
|
if(substituteNameLength == 0 || substituteNameStart + substituteNameLength > rd.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
dest = Encoding.Unicode.GetString(rd, substituteNameStart, substituteNameLength);
|
|
|
|
if(dest.StartsWith(@"\??\", StringComparison.Ordinal)) dest = dest[4..];
|
|
}
|
|
|
|
dest = dest.Replace('\\', '/');
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
case ReparseTag.MountPoint:
|
|
{
|
|
if(dataStart + 8 > rd.Length) return ErrorNumber.InvalidArgument;
|
|
|
|
var printNameOffset = BitConverter.ToUInt16(rd, dataStart + 4);
|
|
var printNameLength = BitConverter.ToUInt16(rd, dataStart + 6);
|
|
|
|
int pathBufferStart = dataStart + 8;
|
|
int printNameStart = pathBufferStart + printNameOffset;
|
|
|
|
if(printNameLength > 0 && printNameStart + printNameLength <= rd.Length)
|
|
dest = Encoding.Unicode.GetString(rd, printNameStart, printNameLength);
|
|
else
|
|
{
|
|
var substituteNameOffset = BitConverter.ToUInt16(rd, dataStart);
|
|
var substituteNameLength = BitConverter.ToUInt16(rd, dataStart + 2);
|
|
|
|
int substituteNameStart = pathBufferStart + substituteNameOffset;
|
|
|
|
if(substituteNameLength == 0 || substituteNameStart + substituteNameLength > rd.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
dest = Encoding.Unicode.GetString(rd, substituteNameStart, substituteNameLength);
|
|
|
|
if(dest.StartsWith(@"\??\", StringComparison.Ordinal)) dest = dest[4..];
|
|
}
|
|
|
|
dest = dest.Replace('\\', '/');
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
case ReparseTag.LxSymlink:
|
|
{
|
|
if(dataStart + 4 > rd.Length) return ErrorNumber.InvalidArgument;
|
|
|
|
int targetStart = dataStart + 4;
|
|
int targetLength = reparseHeader.reparse_data_length - 4;
|
|
|
|
if(targetLength <= 0 || targetStart + targetLength > rd.Length) return ErrorNumber.InvalidArgument;
|
|
|
|
dest = Encoding.UTF8.GetString(rd, targetStart, targetLength);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
default:
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Unsupported reparse tag 0x{0:X8} in MFT record {1}",
|
|
(uint)reparseHeader.reparse_tag,
|
|
mftRecordNumber);
|
|
|
|
return ErrorNumber.NotSupported;
|
|
}
|
|
}
|
|
|
|
/// <summary>Reads uncompressed non-resident file data from data runs with single-cluster caching.</summary>
|
|
/// <param name="mynode">The file node containing data runs and read state.</param>
|
|
/// <param name="length">Number of bytes to read.</param>
|
|
/// <param name="buffer">Destination buffer.</param>
|
|
/// <param name="read">Number of bytes actually read.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber ReadUncompressedFile(NtfsFileNode mynode, long length, byte[] buffer, out long read)
|
|
{
|
|
read = 0;
|
|
long bytesRead = 0;
|
|
|
|
while(bytesRead < length)
|
|
{
|
|
long fileOffset = mynode.Offset;
|
|
long clusterIndex = fileOffset / _bytesPerCluster;
|
|
long offsetInCluster = fileOffset % _bytesPerCluster;
|
|
|
|
// Translate logical cluster to physical cluster via data runs
|
|
long physicalCluster = -1;
|
|
long runStartVcn = 0;
|
|
|
|
foreach((long clusterOffset, long clusterLength) in mynode.DataRuns)
|
|
{
|
|
if(clusterIndex >= runStartVcn && clusterIndex < runStartVcn + clusterLength)
|
|
{
|
|
// Sparse run (offset 0 with no offset bytes stored as 0)
|
|
if(clusterOffset == 0 && clusterLength > 0)
|
|
{
|
|
physicalCluster = 0; // Will be treated as sparse below
|
|
|
|
break;
|
|
}
|
|
|
|
physicalCluster = clusterOffset + (clusterIndex - runStartVcn);
|
|
|
|
break;
|
|
}
|
|
|
|
runStartVcn += clusterLength;
|
|
}
|
|
|
|
// Beyond the end of data runs
|
|
if(physicalCluster < 0) break;
|
|
|
|
long bytesToCopy = Math.Min(length - bytesRead, _bytesPerCluster - offsetInCluster);
|
|
|
|
if(physicalCluster == 0)
|
|
{
|
|
// Sparse cluster — fill with zeros
|
|
Array.Clear(buffer, (int)bytesRead, (int)bytesToCopy);
|
|
}
|
|
else if(mynode.CachedClusterOffset == physicalCluster && mynode.CachedCluster != null)
|
|
{
|
|
// Cache hit — copy from cached cluster
|
|
Array.Copy(mynode.CachedCluster, offsetInCluster, buffer, bytesRead, bytesToCopy);
|
|
}
|
|
else
|
|
{
|
|
// Read cluster from disk
|
|
ulong sectorStart = (ulong)physicalCluster * _sectorsPerCluster;
|
|
|
|
ErrorNumber errno = _image.ReadSectors(_partition.Start + sectorStart,
|
|
false,
|
|
_sectorsPerCluster,
|
|
out byte[] clusterData,
|
|
out _);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading cluster {0}: {1}", physicalCluster, errno);
|
|
|
|
break;
|
|
}
|
|
|
|
// Cache this cluster
|
|
mynode.CachedCluster = clusterData;
|
|
mynode.CachedClusterOffset = physicalCluster;
|
|
|
|
Array.Copy(clusterData, offsetInCluster, buffer, bytesRead, bytesToCopy);
|
|
}
|
|
|
|
bytesRead += bytesToCopy;
|
|
mynode.Offset += bytesToCopy;
|
|
}
|
|
|
|
read = bytesRead;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads compressed non-resident file data by decompressing LZNT1 compression units on demand with caching.
|
|
/// </summary>
|
|
/// <param name="mynode">The file node containing data runs and read state.</param>
|
|
/// <param name="length">Number of bytes to read.</param>
|
|
/// <param name="buffer">Destination buffer.</param>
|
|
/// <param name="read">Number of bytes actually read.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber ReadCompressedFile(NtfsFileNode mynode, long length, byte[] buffer, out long read)
|
|
{
|
|
read = 0;
|
|
long bytesRead = 0;
|
|
int compressionUnitClusters = mynode.CompressionUnitClusters;
|
|
long compressionUnitBytes = compressionUnitClusters * _bytesPerCluster;
|
|
|
|
while(bytesRead < length)
|
|
{
|
|
long fileOffset = mynode.Offset;
|
|
|
|
// Determine which compression unit this offset falls in
|
|
long compressionUnitIndex = fileOffset / compressionUnitBytes;
|
|
long offsetInUnit = fileOffset % compressionUnitBytes;
|
|
long compressionUnitVcn = compressionUnitIndex * compressionUnitClusters;
|
|
|
|
// Check cache first
|
|
if(mynode.CachedCompressionUnitVcn != compressionUnitVcn || mynode.CachedCompressionUnit == null)
|
|
{
|
|
// Need to decompress this compression unit
|
|
ErrorNumber decompressErrno =
|
|
DecompressCompressionUnit(mynode, compressionUnitVcn, compressionUnitClusters, out byte[] unitData);
|
|
|
|
if(decompressErrno != ErrorNumber.NoError) break;
|
|
|
|
mynode.CachedCompressionUnit = unitData;
|
|
mynode.CachedCompressionUnitVcn = compressionUnitVcn;
|
|
}
|
|
|
|
long bytesToCopy = Math.Min(length - bytesRead, compressionUnitBytes - offsetInUnit);
|
|
|
|
// Clamp to what the cached unit actually contains
|
|
if(offsetInUnit + bytesToCopy > mynode.CachedCompressionUnit.Length)
|
|
bytesToCopy = mynode.CachedCompressionUnit.Length - offsetInUnit;
|
|
|
|
if(bytesToCopy <= 0) break;
|
|
|
|
Array.Copy(mynode.CachedCompressionUnit, offsetInUnit, buffer, bytesRead, bytesToCopy);
|
|
|
|
bytesRead += bytesToCopy;
|
|
mynode.Offset += bytesToCopy;
|
|
}
|
|
|
|
read = bytesRead;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads and decompresses a single compression unit from the data runs.</summary>
|
|
/// <param name="mynode">The file node containing data runs.</param>
|
|
/// <param name="unitStartVcn">VCN of the first cluster in the compression unit.</param>
|
|
/// <param name="compressionUnitClusters">Number of clusters in a compression unit.</param>
|
|
/// <param name="unitData">Output decompressed data for the compression unit.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber DecompressCompressionUnit(NtfsFileNode mynode, long unitStartVcn, int compressionUnitClusters,
|
|
out byte[] unitData)
|
|
{
|
|
unitData = null;
|
|
long unitEndVcn = unitStartVcn + compressionUnitClusters;
|
|
|
|
// Collect physical cluster mappings for this compression unit
|
|
// A compression unit may span multiple data runs, and sparse runs within indicate
|
|
// that the remaining clusters in the unit are compressed (or entirely sparse)
|
|
List<(long physicalCluster, long count)> physicalRuns = [];
|
|
long totalClusters = 0;
|
|
var hasSparseRun = false;
|
|
|
|
long runStartVcn = 0;
|
|
|
|
foreach((long clusterOffset, long clusterLength) in mynode.DataRuns)
|
|
{
|
|
long runEndVcn = runStartVcn + clusterLength;
|
|
|
|
// Skip runs entirely before or after this compression unit
|
|
if(runEndVcn <= unitStartVcn)
|
|
{
|
|
runStartVcn = runEndVcn;
|
|
|
|
continue;
|
|
}
|
|
|
|
if(runStartVcn >= unitEndVcn) break;
|
|
|
|
// Clamp run to the compression unit boundaries
|
|
long overlapStart = Math.Max(runStartVcn, unitStartVcn);
|
|
long overlapEnd = Math.Min(runEndVcn, unitEndVcn);
|
|
long overlapCount = overlapEnd - overlapStart;
|
|
|
|
if(clusterOffset == 0)
|
|
{
|
|
// Sparse run
|
|
hasSparseRun = true;
|
|
totalClusters += overlapCount;
|
|
}
|
|
else
|
|
{
|
|
long physStart = clusterOffset + (overlapStart - runStartVcn);
|
|
physicalRuns.Add((physStart, overlapCount));
|
|
totalClusters += overlapCount;
|
|
}
|
|
|
|
runStartVcn = runEndVcn;
|
|
}
|
|
|
|
int unitBytes = compressionUnitClusters * (int)_bytesPerCluster;
|
|
|
|
// If the entire compression unit is sparse, return zeros
|
|
if(physicalRuns.Count == 0)
|
|
{
|
|
unitData = new byte[unitBytes];
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Count physical (non-sparse) clusters
|
|
long physicalCount = 0;
|
|
|
|
foreach((long _, long count) in physicalRuns) physicalCount += count;
|
|
|
|
// If the physical clusters fill the entire compression unit, data is uncompressed
|
|
if(physicalCount >= compressionUnitClusters && !hasSparseRun)
|
|
{
|
|
unitData = new byte[unitBytes];
|
|
var dstOffset = 0;
|
|
|
|
foreach((long physicalCluster, long count) in physicalRuns)
|
|
{
|
|
ulong sectorStart = (ulong)physicalCluster * _sectorsPerCluster;
|
|
var sectorCount = (uint)(count * _sectorsPerCluster);
|
|
|
|
ErrorNumber errno = _image.ReadSectors(_partition.Start + sectorStart,
|
|
false,
|
|
sectorCount,
|
|
out byte[] clusterData,
|
|
out _);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "Error reading clusters at LCN {0}: {1}", physicalCluster, errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
int copyLen = Math.Min(clusterData.Length, unitBytes - dstOffset);
|
|
Array.Copy(clusterData, 0, unitData, dstOffset, copyLen);
|
|
dstOffset += copyLen;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Compressed: read the physical clusters and decompress
|
|
var compressedBytes = (int)(physicalCount * _bytesPerCluster);
|
|
var compressedData = new byte[compressedBytes];
|
|
var compressedOffset = 0;
|
|
|
|
foreach((long physicalCluster, long count) in physicalRuns)
|
|
{
|
|
ulong sectorStart = (ulong)physicalCluster * _sectorsPerCluster;
|
|
var sectorCount = (uint)(count * _sectorsPerCluster);
|
|
|
|
ErrorNumber errno = _image.ReadSectors(_partition.Start + sectorStart,
|
|
false,
|
|
sectorCount,
|
|
out byte[] clusterData,
|
|
out _);
|
|
|
|
if(errno != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Error reading compressed clusters at LCN {0}: {1}",
|
|
physicalCluster,
|
|
errno);
|
|
|
|
return errno;
|
|
}
|
|
|
|
int copyLen = Math.Min(clusterData.Length, compressedBytes - compressedOffset);
|
|
Array.Copy(clusterData, 0, compressedData, compressedOffset, copyLen);
|
|
compressedOffset += copyLen;
|
|
}
|
|
|
|
unitData = DecompressLznt1(compressedData, unitBytes);
|
|
|
|
if(unitData == null)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"LZNT1 decompression failed for compression unit starting at VCN {0}",
|
|
unitStartVcn);
|
|
|
|
return ErrorNumber.InOutError;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads WOF (Windows Overlay Filter) externally compressed file data by decompressing individual frames
|
|
/// using Xpress or LZX algorithms with single-frame caching.
|
|
/// </summary>
|
|
/// <param name="mynode">The file node containing WOF compressed data information.</param>
|
|
/// <param name="length">Number of bytes to read.</param>
|
|
/// <param name="buffer">Destination buffer.</param>
|
|
/// <param name="read">Number of bytes actually read.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber ReadWofFile(NtfsFileNode mynode, long length, byte[] buffer, out long read)
|
|
{
|
|
read = 0;
|
|
long bytesRead = 0;
|
|
int frameSize = mynode.WofFrameSize;
|
|
|
|
// Total number of frames (0-based index of last frame)
|
|
long lastFrameIndex = mynode.Length > 0 ? (mynode.Length - 1) / frameSize : 0;
|
|
|
|
// Offset table size: one entry per frame except the first
|
|
// For files < 4 GiB, entries are 4 bytes; otherwise 8 bytes
|
|
int bytesPerOffset = mynode.Length < 0x100000000L ? 4 : 8;
|
|
long offsetTableSize = lastFrameIndex * bytesPerOffset;
|
|
|
|
while(bytesRead < length)
|
|
{
|
|
long fileOffset = mynode.Offset;
|
|
long frameIndex = fileOffset / frameSize;
|
|
long offsetInFrame = fileOffset % frameSize;
|
|
|
|
// Check cache first
|
|
if(mynode.CachedWofFrameIndex != frameIndex || mynode.CachedWofFrame == null)
|
|
{
|
|
// Need to decompress this frame
|
|
ErrorNumber frameErrno = DecompressWofFrame(mynode,
|
|
frameIndex,
|
|
lastFrameIndex,
|
|
bytesPerOffset,
|
|
offsetTableSize,
|
|
out byte[] frameData);
|
|
|
|
if(frameErrno != ErrorNumber.NoError) break;
|
|
|
|
mynode.CachedWofFrame = frameData;
|
|
mynode.CachedWofFrameIndex = frameIndex;
|
|
}
|
|
|
|
long bytesToCopy = Math.Min(length - bytesRead, mynode.CachedWofFrame.Length - offsetInFrame);
|
|
|
|
if(bytesToCopy <= 0) break;
|
|
|
|
Array.Copy(mynode.CachedWofFrame, offsetInFrame, buffer, bytesRead, bytesToCopy);
|
|
|
|
bytesRead += bytesToCopy;
|
|
mynode.Offset += bytesToCopy;
|
|
}
|
|
|
|
read = bytesRead;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Decompresses a single WOF frame from the WofCompressedData stream.</summary>
|
|
/// <param name="mynode">The file node containing WOF compressed data information.</param>
|
|
/// <param name="frameIndex">Zero-based index of the frame to decompress.</param>
|
|
/// <param name="lastFrameIndex">Zero-based index of the last frame in the file.</param>
|
|
/// <param name="bytesPerOffset">Size of each offset table entry (4 or 8 bytes).</param>
|
|
/// <param name="offsetTableSize">Total size of the offset table in bytes.</param>
|
|
/// <param name="frameData">Output decompressed frame data.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber DecompressWofFrame(NtfsFileNode mynode, long frameIndex, long lastFrameIndex, int bytesPerOffset,
|
|
long offsetTableSize, out byte[] frameData)
|
|
{
|
|
frameData = null;
|
|
|
|
// Determine the compressed frame boundaries from the offset table.
|
|
// The offset table has (lastFrameIndex) entries — one for each frame except the first.
|
|
// Entry[N-1] gives the cumulative end offset of frame N relative to the data area.
|
|
// Frame 0 starts at offset 0 from the data area (no entry needed).
|
|
long frameStart;
|
|
long frameEnd;
|
|
|
|
if(mynode.WofIsResident)
|
|
{
|
|
// Read offset table from resident data
|
|
ErrorNumber offsetErrno = ReadWofOffsetTableResident(mynode.WofResidentData,
|
|
frameIndex,
|
|
lastFrameIndex,
|
|
bytesPerOffset,
|
|
offsetTableSize,
|
|
out frameStart,
|
|
out frameEnd);
|
|
|
|
if(offsetErrno != ErrorNumber.NoError) return offsetErrno;
|
|
|
|
// Read compressed frame data from resident stream
|
|
var srcStart = (int)(offsetTableSize + frameStart);
|
|
var srcLen = (int)(frameEnd - frameStart);
|
|
|
|
if(srcStart + srcLen > mynode.WofResidentData.Length)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME, "WOF resident compressed frame {0} exceeds stream bounds", frameIndex);
|
|
|
|
return ErrorNumber.InOutError;
|
|
}
|
|
|
|
var compressedFrame = new byte[srcLen];
|
|
Array.Copy(mynode.WofResidentData, srcStart, compressedFrame, 0, srcLen);
|
|
|
|
// Determine uncompressed size for this frame
|
|
int uncompressedSize = frameIndex == lastFrameIndex
|
|
? (int)(1 + (mynode.Length - 1) % mynode.WofFrameSize)
|
|
: mynode.WofFrameSize;
|
|
|
|
// If compressed data is same size or larger, it's stored uncompressed
|
|
if(srcLen >= uncompressedSize)
|
|
{
|
|
frameData = new byte[uncompressedSize];
|
|
Array.Copy(compressedFrame, 0, frameData, 0, uncompressedSize);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
frameData = DecompressWofFrameData(compressedFrame, uncompressedSize, mynode.WofAlgorithm);
|
|
|
|
if(frameData == null)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"WOF decompression failed for frame {0} (algorithm {1})",
|
|
frameIndex,
|
|
mynode.WofAlgorithm);
|
|
|
|
return ErrorNumber.InOutError;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Non-resident WofCompressedData — need to read from data runs
|
|
// First read the offset table entry to find where the compressed frame lives
|
|
ErrorNumber nrOffsetErrno = ReadWofOffsetTableNonResident(mynode,
|
|
frameIndex,
|
|
lastFrameIndex,
|
|
bytesPerOffset,
|
|
offsetTableSize,
|
|
out frameStart,
|
|
out frameEnd);
|
|
|
|
if(nrOffsetErrno != ErrorNumber.NoError) return nrOffsetErrno;
|
|
|
|
// Read compressed frame from non-resident stream
|
|
long compressedOffset = offsetTableSize + frameStart;
|
|
var compressedSize = (int)(frameEnd - frameStart);
|
|
|
|
var compressedBuf = new byte[compressedSize];
|
|
|
|
ErrorNumber readErrno = ReadFromDataRuns(mynode.WofDataRuns,
|
|
mynode.WofDataSize,
|
|
compressedOffset,
|
|
compressedBuf,
|
|
compressedSize);
|
|
|
|
if(readErrno != ErrorNumber.NoError) return readErrno;
|
|
|
|
// Determine uncompressed size for this frame
|
|
int uncSize = frameIndex == lastFrameIndex
|
|
? (int)(1 + (mynode.Length - 1) % mynode.WofFrameSize)
|
|
: mynode.WofFrameSize;
|
|
|
|
// If compressed data is same size or larger, it's stored uncompressed
|
|
if(compressedSize >= uncSize)
|
|
{
|
|
frameData = new byte[uncSize];
|
|
Array.Copy(compressedBuf, 0, frameData, 0, uncSize);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
frameData = DecompressWofFrameData(compressedBuf, uncSize, mynode.WofAlgorithm);
|
|
|
|
if(frameData == null)
|
|
{
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"WOF decompression failed for frame {0} (algorithm {1})",
|
|
frameIndex,
|
|
mynode.WofAlgorithm);
|
|
|
|
return ErrorNumber.InOutError;
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads WOF frame offset boundaries from a resident WofCompressedData stream.</summary>
|
|
/// <param name="data">The complete resident WofCompressedData stream.</param>
|
|
/// <param name="frameIndex">Zero-based frame index.</param>
|
|
/// <param name="lastFrameIndex">Zero-based index of the last frame.</param>
|
|
/// <param name="bytesPerOffset">Size of each offset table entry (4 or 8).</param>
|
|
/// <param name="offsetTableSize">Total size of the offset table in bytes.</param>
|
|
/// <param name="frameStart">Output: byte offset where compressed frame starts (relative to data area).</param>
|
|
/// <param name="frameEnd">Output: byte offset where compressed frame ends (relative to data area).</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
static ErrorNumber ReadWofOffsetTableResident(byte[] data, long frameIndex, long lastFrameIndex, int bytesPerOffset,
|
|
long offsetTableSize, out long frameStart, out long frameEnd)
|
|
{
|
|
frameStart = 0;
|
|
frameEnd = 0;
|
|
|
|
if(frameIndex == 0)
|
|
{
|
|
// First frame starts at offset 0 from the data area
|
|
frameStart = 0;
|
|
|
|
if(lastFrameIndex == 0)
|
|
{
|
|
// Only one frame — compressed data extends to end of stream
|
|
frameEnd = data.Length - offsetTableSize;
|
|
}
|
|
else
|
|
{
|
|
// Read offset[0] to get end of first frame
|
|
if(bytesPerOffset == 4)
|
|
frameEnd = BitConverter.ToUInt32(data, 0);
|
|
else
|
|
frameEnd = (long)BitConverter.ToUInt64(data, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Read offset[frameIndex-1] for start and offset[frameIndex] for end
|
|
var prevPos = (int)((frameIndex - 1) * bytesPerOffset);
|
|
|
|
if(bytesPerOffset == 4)
|
|
frameStart = BitConverter.ToUInt32(data, prevPos);
|
|
else
|
|
frameStart = (long)BitConverter.ToUInt64(data, prevPos);
|
|
|
|
if(frameIndex == lastFrameIndex)
|
|
{
|
|
// Last frame — compressed data extends to end of stream
|
|
frameEnd = data.Length - offsetTableSize;
|
|
}
|
|
else
|
|
{
|
|
var curPos = (int)(frameIndex * bytesPerOffset);
|
|
|
|
if(bytesPerOffset == 4)
|
|
frameEnd = BitConverter.ToUInt32(data, curPos);
|
|
else
|
|
frameEnd = (long)BitConverter.ToUInt64(data, curPos);
|
|
}
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads WOF frame offset boundaries from a non-resident WofCompressedData stream.</summary>
|
|
/// <param name="mynode">File node with WOF data runs.</param>
|
|
/// <param name="frameIndex">Zero-based frame index.</param>
|
|
/// <param name="lastFrameIndex">Zero-based index of the last frame.</param>
|
|
/// <param name="bytesPerOffset">Size of each offset table entry (4 or 8).</param>
|
|
/// <param name="offsetTableSize">Total size of the offset table in bytes.</param>
|
|
/// <param name="frameStart">Output: byte offset where compressed frame starts (relative to data area).</param>
|
|
/// <param name="frameEnd">Output: byte offset where compressed frame ends (relative to data area).</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber ReadWofOffsetTableNonResident(NtfsFileNode mynode, long frameIndex, long lastFrameIndex,
|
|
int bytesPerOffset, long offsetTableSize, out long frameStart,
|
|
out long frameEnd)
|
|
{
|
|
frameStart = 0;
|
|
frameEnd = 0;
|
|
|
|
if(frameIndex == 0)
|
|
{
|
|
frameStart = 0;
|
|
|
|
if(lastFrameIndex == 0)
|
|
frameEnd = mynode.WofDataSize - offsetTableSize;
|
|
else
|
|
{
|
|
// Read offset[0]
|
|
var offsetBuf = new byte[bytesPerOffset];
|
|
|
|
ErrorNumber errno =
|
|
ReadFromDataRuns(mynode.WofDataRuns, mynode.WofDataSize, 0, offsetBuf, bytesPerOffset);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
frameEnd = bytesPerOffset == 4
|
|
? BitConverter.ToUInt32(offsetBuf, 0)
|
|
: (long)BitConverter.ToUInt64(offsetBuf, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Read both offset[frameIndex-1] and offset[frameIndex] (if not last frame)
|
|
long prevOff = (frameIndex - 1) * bytesPerOffset;
|
|
int bytesToRead = frameIndex == lastFrameIndex ? bytesPerOffset : bytesPerOffset * 2;
|
|
var offsetBuf = new byte[bytesToRead];
|
|
|
|
ErrorNumber errno =
|
|
ReadFromDataRuns(mynode.WofDataRuns, mynode.WofDataSize, prevOff, offsetBuf, bytesToRead);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
frameStart = bytesPerOffset == 4
|
|
? BitConverter.ToUInt32(offsetBuf, 0)
|
|
: (long)BitConverter.ToUInt64(offsetBuf, 0);
|
|
|
|
if(frameIndex == lastFrameIndex)
|
|
frameEnd = mynode.WofDataSize - offsetTableSize;
|
|
else
|
|
{
|
|
frameEnd = bytesPerOffset == 4
|
|
? BitConverter.ToUInt32(offsetBuf, bytesPerOffset)
|
|
: (long)BitConverter.ToUInt64(offsetBuf, bytesPerOffset);
|
|
}
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <summary>Reads bytes from a non-resident data stream at a given byte offset.</summary>
|
|
/// <param name="dataRuns">The data run list for the stream.</param>
|
|
/// <param name="dataSize">Total logical size of the stream.</param>
|
|
/// <param name="offset">Byte offset within the stream to start reading.</param>
|
|
/// <param name="buffer">Destination buffer.</param>
|
|
/// <param name="count">Number of bytes to read.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber ReadFromDataRuns(List<(long clusterOffset, long clusterLength)> dataRuns, long dataSize, long offset,
|
|
byte[] buffer, int count)
|
|
{
|
|
if(offset + count > dataSize) return ErrorNumber.InvalidArgument;
|
|
|
|
var bytesRead = 0;
|
|
long runStartByte = 0;
|
|
|
|
foreach((long clusterOffset, long clusterLength) in dataRuns)
|
|
{
|
|
long runBytes = clusterLength * _bytesPerCluster;
|
|
long runEndByte = runStartByte + runBytes;
|
|
|
|
if(offset + bytesRead >= runEndByte)
|
|
{
|
|
runStartByte = runEndByte;
|
|
|
|
continue;
|
|
}
|
|
|
|
if(offset + bytesRead < runStartByte)
|
|
{
|
|
runStartByte = runEndByte;
|
|
|
|
continue;
|
|
}
|
|
|
|
long offsetInRun = offset + bytesRead - runStartByte;
|
|
long availableBytes = runBytes - offsetInRun;
|
|
var toRead = (int)Math.Min(count - bytesRead, availableBytes);
|
|
|
|
if(clusterOffset == 0)
|
|
{
|
|
// Sparse run — fill with zeros
|
|
Array.Clear(buffer, bytesRead, toRead);
|
|
bytesRead += toRead;
|
|
}
|
|
else
|
|
{
|
|
long clusterInRun = offsetInRun / _bytesPerCluster;
|
|
long offsetInCluster = offsetInRun % _bytesPerCluster;
|
|
long physicalCluster = clusterOffset + clusterInRun;
|
|
|
|
while(toRead > 0)
|
|
{
|
|
ulong sectorStart = (ulong)physicalCluster * _sectorsPerCluster;
|
|
|
|
ErrorNumber errno = _image.ReadSectors(_partition.Start + sectorStart,
|
|
false,
|
|
_sectorsPerCluster,
|
|
out byte[] clusterData,
|
|
out _);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
var copyLen = (int)Math.Min(toRead, _bytesPerCluster - offsetInCluster);
|
|
Array.Copy(clusterData, offsetInCluster, buffer, bytesRead, copyLen);
|
|
|
|
bytesRead += copyLen;
|
|
toRead -= copyLen;
|
|
physicalCluster++;
|
|
offsetInCluster = 0;
|
|
}
|
|
}
|
|
|
|
runStartByte = runEndByte;
|
|
|
|
if(bytesRead >= count) break;
|
|
}
|
|
|
|
return bytesRead >= count ? ErrorNumber.NoError : ErrorNumber.InOutError;
|
|
}
|
|
|
|
/// <summary>Decompresses a WOF compressed frame using the appropriate algorithm.</summary>
|
|
/// <param name="compressedData">The compressed frame data.</param>
|
|
/// <param name="uncompressedSize">Expected uncompressed size of the frame.</param>
|
|
/// <param name="algorithm">WOF compression algorithm identifier.</param>
|
|
/// <returns>The decompressed data, or <c>null</c> if decompression fails.</returns>
|
|
static byte[] DecompressWofFrameData(byte[] compressedData, int uncompressedSize, uint algorithm)
|
|
{
|
|
return algorithm switch
|
|
{
|
|
WOF_COMPRESSION_XPRESS4K => DecompressXpress(compressedData, uncompressedSize),
|
|
WOF_COMPRESSION_XPRESS8K => DecompressXpress(compressedData, uncompressedSize),
|
|
WOF_COMPRESSION_XPRESS16K => DecompressXpress(compressedData, uncompressedSize),
|
|
WOF_COMPRESSION_LZX32K => DecompressLzx(compressedData, uncompressedSize),
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
/// <summary>Sets timestamps on a <see cref="FileEntryInfo" /> from NTFS FILETIME values.</summary>
|
|
/// <param name="info">The file entry info to populate.</param>
|
|
/// <param name="creationTime">NTFS creation time (FILETIME).</param>
|
|
/// <param name="lastWriteTime">NTFS last data modification time (FILETIME).</param>
|
|
/// <param name="lastMftChangeTime">NTFS last MFT record change time (FILETIME).</param>
|
|
/// <param name="lastAccessTime">NTFS last access time (FILETIME).</param>
|
|
static void SetTimestamps(FileEntryInfo info, long creationTime, long lastWriteTime, long lastMftChangeTime,
|
|
long lastAccessTime)
|
|
{
|
|
if(creationTime > 0) info.CreationTimeUtc = DateTime.FromFileTimeUtc(creationTime);
|
|
|
|
if(lastWriteTime > 0) info.LastWriteTimeUtc = DateTime.FromFileTimeUtc(lastWriteTime);
|
|
|
|
if(lastMftChangeTime > 0) info.StatusChangeTimeUtc = DateTime.FromFileTimeUtc(lastMftChangeTime);
|
|
|
|
if(lastAccessTime > 0) info.AccessTimeUtc = DateTime.FromFileTimeUtc(lastAccessTime);
|
|
}
|
|
|
|
/// <summary>Resolves a normalized path to its MFT record number by traversing the directory tree.</summary>
|
|
/// <param name="normalizedPath">Absolute path starting with '/'.</param>
|
|
/// <param name="mftRecordNumber">Output MFT record number.</param>
|
|
/// <returns>Error number indicating success or failure.</returns>
|
|
ErrorNumber ResolvePathToMftRecord(string normalizedPath, out uint mftRecordNumber)
|
|
{
|
|
mftRecordNumber = 0;
|
|
|
|
string cutPath = normalizedPath[1..]; // Remove leading '/'
|
|
|
|
string[] pieces = cutPath.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if(pieces.Length == 0) return ErrorNumber.NoSuchFile;
|
|
|
|
// Start from root directory
|
|
Dictionary<string, ulong> currentDirectory = _rootDirectoryCache;
|
|
|
|
for(var p = 0; p < pieces.Length; p++)
|
|
{
|
|
string component = pieces[p];
|
|
|
|
if(!currentDirectory.TryGetValue(component, out ulong mftRef)) return ErrorNumber.NoSuchFile;
|
|
|
|
var recordNum = (uint)(mftRef & 0x0000FFFFFFFFFFFF);
|
|
|
|
// If this is the last component, return it
|
|
if(p == pieces.Length - 1)
|
|
{
|
|
mftRecordNumber = recordNum;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Not the last component — must be a directory
|
|
ErrorNumber errno = ReadDirectoryEntries(recordNum, out Dictionary<string, ulong> dirEntries);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
currentDirectory = dirEntries;
|
|
}
|
|
|
|
return ErrorNumber.NoSuchFile;
|
|
}
|
|
|
|
/// <summary>Parses WSL metadata EAs ($LXUID, $LXGID, $LXMOD, $LXDEV) from EA attribute data.</summary>
|
|
/// <param name="data">Buffer containing the EA data.</param>
|
|
/// <param name="start">Start offset of the EA data in the buffer.</param>
|
|
/// <param name="length">Total length of the EA data.</param>
|
|
/// <param name="lxUid">Output UID if $LXUID is found.</param>
|
|
/// <param name="lxGid">Output GID if $LXGID is found.</param>
|
|
/// <param name="lxMod">Output POSIX mode if $LXMOD is found.</param>
|
|
/// <param name="lxDev">Output device number if $LXDEV is found.</param>
|
|
/// <param name="lxAtime">Output last access time if $LXATTRB is found.</param>
|
|
/// <param name="lxMtime">Output last modification time if $LXATTRB is found.</param>
|
|
/// <param name="lxCtime">Output last status change time if $LXATTRB is found.</param>
|
|
static void ParseWslEas(byte[] data, int start, int length, ref uint? lxUid, ref uint? lxGid, ref uint? lxMod,
|
|
ref ulong? lxDev, ref DateTime? lxAtime, ref DateTime? lxMtime, ref DateTime? lxCtime)
|
|
{
|
|
int pos = start;
|
|
int end = start + length;
|
|
int eaHeaderSize = Marshal.SizeOf<EaAttribute>();
|
|
int lxAttrbSize = Marshal.SizeOf<LxAttrb>();
|
|
|
|
// First pass: parse $LXATTRB (WSL1 combined metadata) as base values
|
|
int firstPos = pos;
|
|
|
|
while(firstPos + eaHeaderSize <= end)
|
|
{
|
|
EaAttribute ea = Marshal.ByteArrayToStructureLittleEndian<EaAttribute>(data, firstPos, eaHeaderSize);
|
|
|
|
int nameStart = firstPos + eaHeaderSize;
|
|
|
|
if(nameStart + ea.ea_name_length > end) break;
|
|
|
|
string eaName = Encoding.ASCII.GetString(data, nameStart, ea.ea_name_length);
|
|
|
|
if(eaName == EA_LXATTRB)
|
|
{
|
|
int valueStart = nameStart + ea.ea_name_length + 1;
|
|
|
|
if(ea.ea_value_length >= lxAttrbSize && valueStart + lxAttrbSize <= end)
|
|
{
|
|
LxAttrb lxAttrb = Marshal.ByteArrayToStructureLittleEndian<LxAttrb>(data, valueStart, lxAttrbSize);
|
|
|
|
lxUid = lxAttrb.st_uid;
|
|
lxGid = lxAttrb.st_gid;
|
|
lxMod = lxAttrb.st_mode;
|
|
|
|
if(lxAttrb.st_rdev != 0)
|
|
{
|
|
uint major = lxAttrb.st_rdev >> 8 & 0xFF;
|
|
uint minor = lxAttrb.st_rdev & 0xFF;
|
|
lxDev = (ulong)major << 32 | minor;
|
|
}
|
|
|
|
if(lxAttrb.st_atime != 0)
|
|
{
|
|
lxAtime = DateTimeOffset.FromUnixTimeSeconds(lxAttrb.st_atime)
|
|
.UtcDateTime.AddTicks(lxAttrb.st_atime_nsec / 100);
|
|
}
|
|
|
|
if(lxAttrb.st_mtime != 0)
|
|
{
|
|
lxMtime = DateTimeOffset.FromUnixTimeSeconds(lxAttrb.st_mtime)
|
|
.UtcDateTime.AddTicks(lxAttrb.st_mtime_nsec / 100);
|
|
}
|
|
|
|
if(lxAttrb.st_ctime != 0)
|
|
{
|
|
lxCtime = DateTimeOffset.FromUnixTimeSeconds(lxAttrb.st_ctime)
|
|
.UtcDateTime.AddTicks(lxAttrb.st_ctime_nsec / 100);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if(ea.next_entry_offset == 0) break;
|
|
|
|
firstPos += (int)ea.next_entry_offset;
|
|
}
|
|
|
|
// Second pass: parse individual WSL2 EAs (these override $LXATTRB values)
|
|
while(pos + eaHeaderSize <= end)
|
|
{
|
|
EaAttribute ea = Marshal.ByteArrayToStructureLittleEndian<EaAttribute>(data, pos, eaHeaderSize);
|
|
|
|
int nameStart = pos + eaHeaderSize;
|
|
|
|
if(nameStart + ea.ea_name_length > end) break;
|
|
|
|
string eaName = Encoding.ASCII.GetString(data, nameStart, ea.ea_name_length);
|
|
|
|
// EA value follows the name + NUL terminator
|
|
int valueStart = nameStart + ea.ea_name_length + 1;
|
|
|
|
if(eaName == EA_LXUID && ea.ea_value_length >= 4 && valueStart + 4 <= end)
|
|
lxUid = BitConverter.ToUInt32(data, valueStart);
|
|
else if(eaName == EA_LXGID && ea.ea_value_length >= 4 && valueStart + 4 <= end)
|
|
lxGid = BitConverter.ToUInt32(data, valueStart);
|
|
else if(eaName == EA_LXMOD && ea.ea_value_length >= 4 && valueStart + 4 <= end)
|
|
lxMod = BitConverter.ToUInt32(data, valueStart);
|
|
else if(eaName == EA_LXDEV && ea.ea_value_length >= 8 && valueStart + 8 <= end)
|
|
{
|
|
var major = BitConverter.ToUInt32(data, valueStart);
|
|
var minor = BitConverter.ToUInt32(data, valueStart + 4);
|
|
lxDev = (ulong)major << 32 | minor;
|
|
}
|
|
|
|
if(ea.next_entry_offset == 0) break;
|
|
|
|
pos += (int)ea.next_entry_offset;
|
|
}
|
|
}
|
|
} |