Files
nt-lite/code/drivers/fs/ntfs/ntfs.c
T

1716 lines
67 KiB
C

// Native read-only NTFS filesystem driver for NTLite.
// Portions are based on ntfs-efi by Mark Harmstone (2023), GPLv2-or-later.
// The port removes UEFI protocol/header dependencies and targets NTLite's IO_FILESYSTEM ABI.
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <ntlite/ddi/io_driver.h>
#include <ntlite/ddi/hal_api.h>
#include <kernel/io_file.h>
#include <kernel/mm_virt.h>
#include <kernel/ke_debug.h>
#include <kernel/ke_list.h>
#include <kernel/ke_spinlock.h>
#define NTFS_MAX_VOLUMES 8u
#define NTFS_MAX_PARTITIONS_PER_DISK 32u
#define NTFS_SECTOR_SIZE 512u
#define NTFS_FS_NAME "NTFS "
#define NTFS_UPCASE_TABLE_CHARS 0x10000u
#define NTFS_UPCASE_TABLE_BYTES (NTFS_UPCASE_TABLE_CHARS * sizeof(uint16_t))
#ifndef NTFS_TRACE_ENABLED
#define NTFS_TRACE_ENABLED 0
#endif
#if NTFS_TRACE_ENABLED
#define NtfsTrace DbgPrint
#else
#define NtfsTrace(...) ((void)0)
#endif
#define NTFS_MFT_INODE 0ull
#define NTFS_ROOT_DIR_INODE 5ull
#define NTFS_UPCASE_INODE 10ull
#define NTFS_FILE_SIGNATURE 0x454c4946u /* FILE */
#define NTFS_INDEX_SIGNATURE 0x58444e49u /* INDX */
#define NTFS_ATTR_STANDARD_INFORMATION 0x10u
#define NTFS_ATTR_ATTRIBUTE_LIST 0x20u
#define NTFS_ATTR_FILE_NAME 0x30u
#define NTFS_ATTR_DATA 0x80u
#define NTFS_ATTR_INDEX_ROOT 0x90u
#define NTFS_ATTR_INDEX_ALLOCATION 0xA0u
#define NTFS_ATTR_END 0xffffffffu
#define NTFS_FORM_RESIDENT 0u
#define NTFS_FORM_NONRESIDENT 1u
#define NTFS_ATTRIBUTE_FLAG_COMPRESSION_MASK 0x00ffu
#define NTFS_ATTRIBUTE_FLAG_ENCRYPTED 0x4000u
#define NTFS_FILE_ATTRIBUTE_READONLY 0x00000001u
#define NTFS_FILE_ATTRIBUTE_HIDDEN 0x00000002u
#define NTFS_FILE_ATTRIBUTE_SYSTEM 0x00000004u
#define NTFS_FILE_ATTRIBUTE_DIRECTORY 0x00000010u
#define NTFS_FILE_ATTRIBUTE_ARCHIVE 0x00000020u
#define NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT 0x10000000u
#define NTFS_INDEX_ENTRY_SUBNODE 1u
#define NTFS_INDEX_ENTRY_LAST 2u
#define NTFS_FILE_NAME_POSIX 0u
#define NTFS_FILE_NAME_WINDOWS 1u
#define NTFS_FILE_NAME_DOS 2u
#define NTFS_FILE_NAME_WINDOWS_AND_DOS 3u
#pragma pack(push, 1)
typedef struct NTFS_BOOT_SECTOR {
uint8_t Jmp[3];
uint8_t FsName[8];
uint16_t BytesPerSector;
uint8_t SectorsPerCluster;
uint16_t ReservedSectors;
uint8_t Unused1[5];
uint8_t Media;
uint8_t Unused2[2];
uint16_t SectorsPerTrack;
uint16_t Heads;
uint32_t HiddenSectors;
uint32_t Unused3;
uint32_t Unknown;
uint64_t TotalSectors;
uint64_t MFT;
uint64_t MFTMirr;
int8_t ClustersPerMFTRecord;
uint8_t Padding1[3];
int8_t ClustersPerIndexRecord;
uint8_t Padding2[3];
uint64_t SerialNumber;
uint32_t Checksum;
} NTFS_BOOT_SECTOR;
typedef struct NTFS_MULTI_SECTOR_HEADER {
uint32_t Signature;
uint16_t UpdateSequenceArrayOffset;
uint16_t UpdateSequenceArraySize;
} NTFS_MULTI_SECTOR_HEADER;
typedef struct NTFS_FILE_RECORD_SEGMENT_HEADER {
NTFS_MULTI_SECTOR_HEADER MultiSectorHeader;
uint64_t LogFileSequenceNumber;
uint16_t SequenceNumber;
uint16_t HardLinkCount;
uint16_t FirstAttributeOffset;
uint16_t Flags;
uint32_t EntryUsedSize;
uint32_t EntryAllocatedSize;
uint64_t BaseFileRecordSegment;
uint16_t NextAttributeID;
} NTFS_FILE_RECORD_SEGMENT_HEADER;
typedef struct NTFS_ATTRIBUTE_RECORD_HEADER {
uint32_t TypeCode;
uint16_t RecordLength;
uint16_t RecordLengthHigh;
uint8_t FormCode;
uint8_t NameLength;
uint16_t NameOffset;
uint16_t Flags;
uint16_t Instance;
union {
struct {
uint32_t ValueLength;
uint16_t ValueOffset;
uint8_t Reserved[2];
} Resident;
struct {
uint64_t LowestVcn;
uint64_t HighestVcn;
uint16_t MappingPairsOffset;
uint16_t CompressionUnit;
uint32_t Padding;
uint64_t AllocatedLength;
uint64_t FileSize;
uint64_t ValidDataLength;
uint64_t TotalAllocated;
} Nonresident;
} Form;
} NTFS_ATTRIBUTE_RECORD_HEADER;
typedef struct NTFS_STANDARD_INFORMATION {
int64_t CreationTime;
int64_t LastAccessTime;
int64_t LastWriteTime;
int64_t ChangeTime;
uint32_t FileAttributes;
uint32_t MaximumVersions;
uint32_t VersionNumber;
uint32_t ClassId;
uint32_t OwnerId;
uint32_t SecurityId;
uint64_t QuotaCharged;
uint64_t USN;
} NTFS_STANDARD_INFORMATION;
typedef struct NTFS_INDEX_NODE_HEADER {
uint32_t FirstEntry;
uint32_t TotalSize;
uint32_t AllocatedSize;
uint32_t Flags;
} NTFS_INDEX_NODE_HEADER;
typedef struct NTFS_INDEX_ENTRY {
uint64_t FileReference;
uint16_t EntryLength;
uint16_t StreamLength;
uint32_t Flags;
} NTFS_INDEX_ENTRY;
typedef struct NTFS_INDEX_ROOT {
uint32_t AttributeType;
uint32_t CollationRule;
uint32_t BytesPerIndexRecord;
uint8_t ClustersPerIndexRecord;
uint8_t Padding[3];
NTFS_INDEX_NODE_HEADER NodeHeader;
} NTFS_INDEX_ROOT;
typedef struct NTFS_INDEX_RECORD {
NTFS_MULTI_SECTOR_HEADER MultiSectorHeader;
uint64_t SequenceNumber;
uint64_t Vcn;
NTFS_INDEX_NODE_HEADER Header;
uint16_t UpdateSequence;
} NTFS_INDEX_RECORD;
typedef struct NTFS_FILE_NAME_ATTR {
uint64_t Parent;
int64_t CreationTime;
int64_t LastAccessTime;
int64_t LastWriteTime;
int64_t ChangeTime;
uint64_t AllocationSize;
uint64_t EndOfFile;
uint32_t FileAttributes;
uint32_t EaSize;
uint8_t FileNameLength;
uint8_t Namespace;
uint16_t FileName[1];
} NTFS_FILE_NAME_ATTR;
typedef struct NTFS_GPT_GUID {
uint32_t Data1;
uint16_t Data2;
uint16_t Data3;
uint8_t Data4[8];
} NTFS_GPT_GUID;
typedef struct NTFS_GPT_HEADER {
char Signature[8];
uint32_t Revision;
uint32_t HeaderSize;
uint32_t HeaderCRC32;
uint32_t Reserved;
uint64_t MyLBA;
uint64_t AlternateLBA;
uint64_t FirstUsableLBA;
uint64_t LastUsableLBA;
NTFS_GPT_GUID DiskGUID;
uint64_t PartitionEntryLBA;
uint32_t NumberOfPartitionEntries;
uint32_t SizeOfPartitionEntry;
uint32_t PartitionEntryArrayCRC32;
} NTFS_GPT_HEADER;
typedef struct NTFS_GPT_ENTRY {
NTFS_GPT_GUID PartitionTypeGUID;
NTFS_GPT_GUID UniquePartitionGUID;
uint64_t StartingLBA;
uint64_t EndingLBA;
uint64_t Attributes;
uint16_t PartitionName[36];
} NTFS_GPT_ENTRY;
typedef struct NTFS_MBR_PARTITION {
uint8_t BootIndicator;
uint8_t StartingChs[3];
uint8_t OsType;
uint8_t EndingChs[3];
uint32_t StartingLba;
uint32_t SizeInLba;
} NTFS_MBR_PARTITION;
#pragma pack(pop)
typedef struct NTFS_RUN {
LIST_ENTRY Link;
uint64_t Vcn;
uint64_t Lcn;
uint64_t Length;
bool Sparse;
} NTFS_RUN;
typedef struct NTFS_VOLUME {
uint8_t DriveIndex;
uint8_t VolumeIndex;
uint64_t PartitionLba;
uint64_t PartitionSize;
uint64_t SerialNumber;
uint32_t BytesPerSector;
uint32_t SectorsPerCluster;
uint32_t BytesPerCluster;
uint64_t TotalSectors;
uint64_t MftStartCluster;
uint64_t FileRecordSize;
LIST_ENTRY MftRuns;
uint16_t* Upcase;
} NTFS_VOLUME;
typedef struct NTFS_FILE {
NTFS_VOLUME* Vol;
uint64_t Inode;
uint64_t Size;
uint64_t ValidDataLength;
uint64_t PhysicalSize;
uint32_t Attributes;
bool IsDirectory;
NTFS_STANDARD_INFORMATION StandardInfo;
LIST_ENTRY DataRuns;
LIST_ENTRY IndexRuns;
uint8_t* ResidentData;
size_t ResidentDataSize;
NTFS_INDEX_ROOT* IndexRoot;
size_t IndexRootSize;
} NTFS_FILE;
typedef struct NTFS_DIRENT_CACHE {
uint64_t Inode;
uint64_t FileSize;
uint64_t AllocSize;
uint32_t Attributes;
int64_t CreationTime;
int64_t LastAccessTime;
int64_t LastWriteTime;
char Name[SYS_DIR_NAME_MAX];
} NTFS_DIRENT_CACHE;
typedef struct NTFS_DIR_CTX {
NTFS_DIRENT_CACHE* Entries;
uint32_t Count;
uint32_t Capacity;
uint32_t Position;
} NTFS_DIR_CTX;
typedef struct NTFS_PARTITION_CANDIDATE {
uint64_t StartLba;
uint64_t SectorCount;
} NTFS_PARTITION_CANDIDATE;
typedef struct NTFS_ATTR_VIEW {
const NTFS_ATTRIBUTE_RECORD_HEADER* Attr;
const uint8_t* ResidentData;
size_t ResidentSize;
} NTFS_ATTR_VIEW;
typedef struct NTFS_FIND_CTX {
NTFS_VOLUME* Vol;
const uint16_t* Name;
size_t NameLength;
uint64_t Inode;
bool Found;
} NTFS_FIND_CTX;
typedef struct NTFS_COLLECT_CTX {
NTFS_VOLUME* Vol;
NTFS_DIR_CTX* Dir;
KSTATUS Status;
bool HideNtfsMetadata;
} NTFS_COLLECT_CTX;
static NTFS_VOLUME gVolumes[NTFS_MAX_VOLUMES];
static IO_FILESYSTEM gFilesystems[NTFS_MAX_VOLUMES];
static bool gVolumeMounted[NTFS_MAX_VOLUMES];
static bool gVolumeBusy[NTFS_MAX_VOLUMES];
static bool gDiskScanned[NTFS_MAX_VOLUMES];
static SPINLOCK gNtfsMountLock = SPINLOCK_INIT;
static uint32_t gMountedCount;
static uint64_t NtfsAlignUp(uint64_t value, uint64_t alignment) {
if (alignment == 0) return value;
uint64_t rem = value % alignment;
return rem ? value + (alignment - rem) : value;
}
static bool NtfsIsPowerOfTwo32(uint32_t value) {
return value != 0 && (value & (value - 1u)) == 0;
}
static uint64_t NtfsFileReferenceNumber(uint64_t reference) {
return reference & 0x0000ffffffffffffull;
}
static void NtfsInitRunList(LIST_ENTRY* list) {
LIST_INIT(list);
}
static void NtfsFreeRuns(LIST_ENTRY* list) {
while (!LIST_IS_EMPTY(list)) {
LIST_ENTRY* node = ListPopHead(list);
NTFS_RUN* run = CONTAINING_RECORD(node, NTFS_RUN, Link);
MmFreePool(run);
}
}
static void NtfsDebugDumpRuns(const char* tag, const LIST_ENTRY* runs) {
if (!tag || !runs) return;
uint32_t i = 0;
for (LIST_ENTRY* node = runs->Fwd; node != runs && i < 16u; node = node->Fwd, i++) {
NTFS_RUN* run = CONTAINING_RECORD(node, NTFS_RUN, Link);
NtfsTrace("NTFS: %s run[%u]: vcn=%llu lcn=%llu len=%llu sparse=%u\n",
tag, i,
(unsigned long long)run->Vcn,
(unsigned long long)run->Lcn,
(unsigned long long)run->Length,
run->Sparse ? 1u : 0u);
}
if (i == 0) NtfsTrace("NTFS: %s runlist empty\n", tag);
}
static KSTATUS NtfsReadSectors(NTFS_VOLUME* vol, uint64_t lba, uint32_t count, void* buffer) {
if (!vol || !buffer || count == 0) return STATUS_INVALID_PARAMETER;
if (vol->PartitionSize && lba >= vol->PartitionSize) return STATUS_DEVICE_ERROR;
if (vol->PartitionSize && (uint64_t)count > vol->PartitionSize - lba) return STATUS_DEVICE_ERROR;
return IoReadDiskSectors(vol->DriveIndex, vol->PartitionLba + lba, count, buffer);
}
static KSTATUS NtfsReadBytesDirect(NTFS_VOLUME* vol, uint64_t byteOffset, uint64_t byteCount, void* buffer) {
if (!vol || !buffer) return STATUS_INVALID_PARAMETER;
if ((byteOffset % vol->BytesPerSector) != 0 || (byteCount % vol->BytesPerSector) != 0)
return STATUS_INVALID_PARAMETER;
uint64_t lba = byteOffset / vol->BytesPerSector;
uint64_t sectors = byteCount / vol->BytesPerSector;
if (sectors > 0xffffffffull) return STATUS_INVALID_PARAMETER;
return NtfsReadSectors(vol, lba, (uint32_t)sectors, buffer);
}
static KSTATUS NtfsProcessFixups(NTFS_MULTI_SECTOR_HEADER* header, uint64_t length, uint32_t sectorSize) {
if (!header || sectorSize == 0 || length < sectorSize) return STATUS_INVALID_PARAMETER;
uint64_t sectors = length / sectorSize;
if (header->UpdateSequenceArraySize < sectors + 1) return STATUS_INVALID_PARAMETER;
if (header->UpdateSequenceArrayOffset + (uint64_t)header->UpdateSequenceArraySize * sizeof(uint16_t) > length)
return STATUS_INVALID_PARAMETER;
uint16_t* seq = (uint16_t*)((uint8_t*)header + header->UpdateSequenceArrayOffset);
uint8_t* ptr = (uint8_t*)header + sectorSize - sizeof(uint16_t);
for (uint64_t i = 0; i < sectors; i++) {
if (*(uint16_t*)ptr != seq[0]) return STATUS_CHECKSUM_MISMATCH;
*(uint16_t*)ptr = seq[i + 1u];
ptr += sectorSize;
}
return STATUS_SUCCESS;
}
static int64_t NtfsReadSignedLe(const uint8_t* p, uint8_t bytes) {
uint64_t value = 0;
for (uint8_t i = 0; i < bytes; i++) value |= ((uint64_t)p[i]) << (8u * i);
if (bytes && (p[bytes - 1u] & 0x80u)) {
for (uint8_t i = bytes; i < 8u; i++) value |= 0xffull << (8u * i);
}
return (int64_t)value;
}
static uint32_t NtfsAttributeRecordLength(const NTFS_ATTRIBUTE_RECORD_HEADER* attr) {
if (!attr) return 0;
return (uint32_t)attr->RecordLength | ((uint32_t)attr->RecordLengthHigh << 16);
}
static uint32_t NtfsAttributeMinimumLength(uint8_t formCode) {
if (formCode == NTFS_FORM_RESIDENT) {
return (uint32_t)(offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form.Resident.Reserved) +
sizeof(((NTFS_ATTRIBUTE_RECORD_HEADER*)0)->Form.Resident.Reserved));
}
if (formCode == NTFS_FORM_NONRESIDENT) {
return (uint32_t)(offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form.Nonresident.ValidDataLength) +
sizeof(((NTFS_ATTRIBUTE_RECORD_HEADER*)0)->Form.Nonresident.ValidDataLength));
}
return (uint32_t)offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form);
}
static KSTATUS NtfsReadMappings(NTFS_VOLUME* vol, const NTFS_ATTRIBUTE_RECORD_HEADER* attr, LIST_ENTRY* runs) {
if (!vol || !attr || !runs) return STATUS_INVALID_PARAMETER;
if (attr->FormCode != NTFS_FORM_NONRESIDENT) return STATUS_INVALID_PARAMETER;
if (attr->Flags & NTFS_ATTRIBUTE_FLAG_ENCRYPTED) return STATUS_NOT_SUPPORTED;
if (attr->Flags & NTFS_ATTRIBUTE_FLAG_COMPRESSION_MASK) return STATUS_NOT_SUPPORTED;
uint32_t attrLen = NtfsAttributeRecordLength(attr);
if (attrLen < NtfsAttributeMinimumLength(NTFS_FORM_NONRESIDENT)) return STATUS_INVALID_PARAMETER;
if (attr->Form.Nonresident.MappingPairsOffset >= attrLen) return STATUS_INVALID_PARAMETER;
uint64_t limitBytes = attr->Form.Nonresident.AllocatedLength;
if (limitBytes == 0) limitBytes = attr->Form.Nonresident.FileSize;
if (limitBytes == 0) limitBytes = attr->Form.Nonresident.ValidDataLength;
uint64_t maxCluster = NtfsAlignUp(limitBytes, vol->BytesPerCluster) / vol->BytesPerCluster;
if (maxCluster == 0) return STATUS_SUCCESS;
const uint8_t* stream = (const uint8_t*)attr + attr->Form.Nonresident.MappingPairsOffset;
const uint8_t* end = (const uint8_t*)attr + attrLen;
uint64_t nextVcn = attr->Form.Nonresident.LowestVcn;
int64_t currentLcn = 0;
while (stream < end && *stream != 0) {
uint8_t header = *stream++;
uint8_t lenBytes = header & 0x0fu;
uint8_t lcnBytes = header >> 4;
if (lenBytes == 0 || lenBytes > 8 || lcnBytes > 8) return STATUS_INVALID_PARAMETER;
if ((size_t)(end - stream) < (size_t)lenBytes + (size_t)lcnBytes) return STATUS_INVALID_PARAMETER;
int64_t runClustersSigned = NtfsReadSignedLe(stream, lenBytes);
stream += lenBytes;
if (runClustersSigned <= 0) return STATUS_INVALID_PARAMETER;
uint64_t currentVcn = nextVcn;
nextVcn += (uint64_t)runClustersSigned;
if (nextVcn > maxCluster) nextVcn = maxCluster;
NTFS_RUN* run = (NTFS_RUN*)MmAllocatePool(sizeof(NTFS_RUN));
if (!run) return STATUS_NO_MEMORY;
memset(run, 0, sizeof(*run));
run->Vcn = currentVcn;
run->Length = nextVcn - currentVcn;
if (lcnBytes == 0) {
run->Sparse = true;
run->Lcn = 0;
} else {
currentLcn += NtfsReadSignedLe(stream, lcnBytes);
stream += lcnBytes;
if (currentLcn < 0) { MmFreePool(run); return STATUS_INVALID_PARAMETER; }
run->Sparse = false;
run->Lcn = (uint64_t)currentLcn;
}
ListInsertTail(runs, &run->Link);
if (nextVcn >= maxCluster) break;
}
return STATUS_SUCCESS;
}
static KSTATUS NtfsReadFromRuns(NTFS_VOLUME* vol, const LIST_ENTRY* runs, uint64_t offset, void* buffer, uint64_t size) {
if (!vol || !runs || (!buffer && size)) return STATUS_INVALID_PARAMETER;
if (size == 0) return STATUS_SUCCESS;
uint8_t* out = (uint8_t*)buffer;
uint64_t clusterSize = vol->BytesPerCluster;
uint64_t vcn = offset / clusterSize;
uint64_t endOffset = offset + size;
uint64_t lastVcn = NtfsAlignUp(endOffset, clusterSize) / clusterSize;
for (LIST_ENTRY* node = runs->Fwd; node != runs && size; node = node->Fwd) {
NTFS_RUN* run = CONTAINING_RECORD(node, NTFS_RUN, Link);
if (run->Vcn >= lastVcn || run->Vcn + run->Length <= vcn) continue;
uint64_t runByteStart = run->Vcn * clusterSize;
uint64_t runByteEnd = (run->Vcn + run->Length) * clusterSize;
if (offset < runByteStart) {
uint64_t gap = runByteStart - offset;
if (gap > size) gap = size;
memset(out, 0, (size_t)gap);
out += gap;
offset += gap;
size -= gap;
vcn = offset / clusterSize;
if (!size) break;
}
uint64_t chunkStart = offset > runByteStart ? offset : runByteStart;
uint64_t chunkEnd = endOffset < runByteEnd ? endOffset : runByteEnd;
if (chunkEnd <= chunkStart) continue;
uint64_t chunkSize = chunkEnd - chunkStart;
uint64_t mappingOffset = chunkStart - runByteStart;
if (run->Sparse) {
memset(out, 0, (size_t)chunkSize);
} else {
uint64_t byteOnVolume = run->Lcn * clusterSize + mappingOffset;
KSTATUS s = NtfsReadBytesDirect(vol, byteOnVolume, chunkSize, out);
if (!KSUCCESS(s)) return s;
}
out += chunkSize;
offset += chunkSize;
size -= chunkSize;
vcn = offset / clusterSize;
}
if (size) memset(out, 0, (size_t)size);
return STATUS_SUCCESS;
}
static bool NtfsAttrNameEqualsAscii(const NTFS_ATTRIBUTE_RECORD_HEADER* attr, const char* ascii) {
if (!attr || !ascii) return false;
size_t len = strlen(ascii);
if (attr->NameLength != len) return false;
if (attr->NameLength == 0) return len == 0;
uint32_t attrLen = NtfsAttributeRecordLength(attr);
uint32_t nameBytes = (uint32_t)attr->NameLength * sizeof(uint16_t);
if (attr->NameOffset < offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form) ||
attr->NameOffset + nameBytes > attrLen) {
return false;
}
const uint16_t* name = (const uint16_t*)((const uint8_t*)attr + attr->NameOffset);
for (size_t i = 0; i < len; i++) {
if (name[i] != (uint16_t)(uint8_t)ascii[i]) return false;
}
return true;
}
static KSTATUS NtfsForEachRecordAttribute(NTFS_VOLUME* vol,
uint64_t inode,
const NTFS_FILE_RECORD_SEGMENT_HEADER* record,
bool (*callback)(const NTFS_ATTR_VIEW* view, void* ctx),
void* ctx) {
if (!vol || !record || !callback) return STATUS_INVALID_PARAMETER;
uint32_t offset = record->FirstAttributeOffset;
uint32_t recordBytes = record->EntryUsedSize ? record->EntryUsedSize : record->EntryAllocatedSize;
if (recordBytes == 0 || recordBytes > vol->FileRecordSize) recordBytes = (uint32_t)vol->FileRecordSize;
if (offset >= recordBytes || offset < sizeof(NTFS_FILE_RECORD_SEGMENT_HEADER)) {
NtfsTrace("NTFS: attrs: inode=%llu bad firstAttr=%u recordBytes=%u fileRecordSize=%llu\n",
(unsigned long long)inode, offset, recordBytes, (unsigned long long)vol->FileRecordSize);
return STATUS_INVALID_PARAMETER;
}
while (offset + offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form) <= recordBytes) {
const NTFS_ATTRIBUTE_RECORD_HEADER* attr = (const NTFS_ATTRIBUTE_RECORD_HEADER*)((const uint8_t*)record + offset);
if (attr->TypeCode == NTFS_ATTR_END) break;
uint32_t attrLen = NtfsAttributeRecordLength(attr);
uint32_t minAttrLen = NtfsAttributeMinimumLength(attr->FormCode);
if (attrLen < minAttrLen || attrLen == 0) {
NtfsTrace("NTFS: attrs: inode=%llu bad attr off=%u type=0x%x len=%u form=%u min=%u\n",
(unsigned long long)inode, offset, attr->TypeCode, attrLen, attr->FormCode, minAttrLen);
return STATUS_INVALID_PARAMETER;
}
if (offset + attrLen > recordBytes) {
NtfsTrace("NTFS: attrs: inode=%llu attr overruns off=%u type=0x%x len=%u recordBytes=%u used=%u alloc=%u\n",
(unsigned long long)inode, offset, attr->TypeCode, attrLen, recordBytes,
record->EntryUsedSize, record->EntryAllocatedSize);
return STATUS_INVALID_PARAMETER;
}
NTFS_ATTR_VIEW view;
memset(&view, 0, sizeof(view));
view.Attr = attr;
if (attr->FormCode == NTFS_FORM_RESIDENT) {
if ((uint32_t)attr->Form.Resident.ValueOffset < minAttrLen ||
(uint32_t)attr->Form.Resident.ValueOffset + attr->Form.Resident.ValueLength > attrLen) {
NtfsTrace("NTFS: attrs: inode=%llu resident bounds off=%u type=0x%x attrLen=%u valueOff=%u valueLen=%u\n",
(unsigned long long)inode, offset, attr->TypeCode, attrLen,
attr->Form.Resident.ValueOffset, attr->Form.Resident.ValueLength);
return STATUS_INVALID_PARAMETER;
}
view.ResidentData = (const uint8_t*)attr + attr->Form.Resident.ValueOffset;
view.ResidentSize = attr->Form.Resident.ValueLength;
} else if (attr->FormCode != NTFS_FORM_NONRESIDENT) {
NtfsTrace("NTFS: attrs: inode=%llu unsupported form=%u off=%u type=0x%x\n",
(unsigned long long)inode, attr->FormCode, offset, attr->TypeCode);
return STATUS_INVALID_PARAMETER;
}
if (!callback(&view, ctx)) break;
offset += attrLen;
}
return STATUS_SUCCESS;
}
static KSTATUS NtfsValidateFileRecord(NTFS_VOLUME* vol, uint64_t inode, void* record, const char* source) {
NTFS_FILE_RECORD_SEGMENT_HEADER* fr = (NTFS_FILE_RECORD_SEGMENT_HEADER*)record;
if (fr->MultiSectorHeader.Signature != NTFS_FILE_SIGNATURE) {
const uint8_t* b = (const uint8_t*)record;
NtfsTrace("NTFS: record: inode=%llu %s bad signature=0x%x bytes=%02x %02x %02x %02x mftOff=%llu\n",
(unsigned long long)inode, source ? source : "?", fr->MultiSectorHeader.Signature,
b[0], b[1], b[2], b[3], (unsigned long long)(inode * vol->FileRecordSize));
return STATUS_INVALID_PARAMETER;
}
KSTATUS s = NtfsProcessFixups(&fr->MultiSectorHeader, vol->FileRecordSize, vol->BytesPerSector);
if (!KSUCCESS(s)) {
NtfsTrace("NTFS: record: inode=%llu %s fixup failed st=0x%x sig=0x%x usaOff=%u usaCount=%u recordSize=%llu sector=%u\n",
(unsigned long long)inode, source ? source : "?", s, fr->MultiSectorHeader.Signature,
fr->MultiSectorHeader.UpdateSequenceArrayOffset,
fr->MultiSectorHeader.UpdateSequenceArraySize,
(unsigned long long)vol->FileRecordSize, vol->BytesPerSector);
return s;
}
return STATUS_SUCCESS;
}
static KSTATUS NtfsReadRecord(NTFS_VOLUME* vol, uint64_t inode, void* record) {
if (!vol || !record) return STATUS_INVALID_PARAMETER;
uint64_t mftOffset = inode * vol->FileRecordSize;
KSTATUS s = NtfsReadFromRuns(vol, &vol->MftRuns, mftOffset, record, vol->FileRecordSize);
if (!KSUCCESS(s)) {
NtfsTrace("NTFS: record: inode=%llu runlist read failed st=0x%x mftOff=%llu size=%llu\n",
(unsigned long long)inode, s, (unsigned long long)mftOffset,
(unsigned long long)vol->FileRecordSize);
NtfsDebugDumpRuns("mft", &vol->MftRuns);
return s;
}
s = NtfsValidateFileRecord(vol, inode, record, "runlist");
if (KSUCCESS(s)) return s;
/* Diagnostic and conservative rescue path: on a normal NTFS volume, early MFT records
are contiguous from the boot-sector $MFT LCN. If the decoded runlist gives garbage,
try that direct address and log whether it works. This also keeps the system usable
while we narrow the runlist bug down. */
uint8_t* direct = (uint8_t*)MmAllocatePool((size_t)vol->FileRecordSize);
if (!direct) return s;
uint64_t directByte = vol->MftStartCluster * vol->BytesPerCluster + mftOffset;
KSTATUS ds = NtfsReadBytesDirect(vol, directByte, vol->FileRecordSize, direct);
if (KSUCCESS(ds)) ds = NtfsValidateFileRecord(vol, inode, direct, "boot-mft-direct");
else NtfsTrace("NTFS: record: inode=%llu direct read failed st=0x%x byte=%llu\n",
(unsigned long long)inode, ds, (unsigned long long)directByte);
if (KSUCCESS(ds)) {
memcpy(record, direct, (size_t)vol->FileRecordSize);
NtfsTrace("NTFS: record: inode=%llu used boot-sector $MFT direct fallback; decoded MFT runlist is suspect\n",
(unsigned long long)inode);
NtfsDebugDumpRuns("mft", &vol->MftRuns);
MmFreePool(direct);
return STATUS_SUCCESS;
}
MmFreePool(direct);
return s;
}
static void NtfsInitFile(NTFS_FILE* file, NTFS_VOLUME* vol, uint64_t inode) {
memset(file, 0, sizeof(*file));
file->Vol = vol;
file->Inode = inode;
NtfsInitRunList(&file->DataRuns);
NtfsInitRunList(&file->IndexRuns);
}
static void NtfsCleanupFile(NTFS_FILE* file) {
if (!file) return;
NtfsFreeRuns(&file->DataRuns);
NtfsFreeRuns(&file->IndexRuns);
if (file->ResidentData) MmFreePool(file->ResidentData);
if (file->IndexRoot) MmFreePool(file->IndexRoot);
memset(file, 0, sizeof(*file));
}
typedef struct NTFS_LOAD_FILE_CTX {
NTFS_FILE* File;
KSTATUS Status;
} NTFS_LOAD_FILE_CTX;
static bool NtfsLoadFileAttributeCallback(const NTFS_ATTR_VIEW* view, void* rawCtx) {
NTFS_LOAD_FILE_CTX* ctx = (NTFS_LOAD_FILE_CTX*)rawCtx;
NTFS_FILE* file = ctx->File;
const NTFS_ATTRIBUTE_RECORD_HEADER* attr = view->Attr;
switch (attr->TypeCode) {
case NTFS_ATTR_STANDARD_INFORMATION: {
if (attr->FormCode == NTFS_FORM_RESIDENT && view->ResidentData) {
size_t toCopy = view->ResidentSize < sizeof(file->StandardInfo) ? view->ResidentSize : sizeof(file->StandardInfo);
memcpy(&file->StandardInfo, view->ResidentData, toCopy);
file->Attributes = file->StandardInfo.FileAttributes;
}
break;
}
case NTFS_ATTR_FILE_NAME: {
if (attr->FormCode == NTFS_FORM_RESIDENT && view->ResidentSize >= offsetof(NTFS_FILE_NAME_ATTR, FileName)) {
const NTFS_FILE_NAME_ATTR* fn = (const NTFS_FILE_NAME_ATTR*)view->ResidentData;
file->Attributes = fn->FileAttributes;
file->IsDirectory = (fn->FileAttributes & NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT) != 0 ||
(fn->FileAttributes & NTFS_FILE_ATTRIBUTE_DIRECTORY) != 0;
if (file->Size == 0) file->Size = fn->EndOfFile;
if (file->PhysicalSize == 0) file->PhysicalSize = fn->AllocationSize;
}
break;
}
case NTFS_ATTR_DATA: {
if (attr->NameLength != 0) break;
if (attr->FormCode == NTFS_FORM_RESIDENT) {
file->Size = view->ResidentSize;
file->ValidDataLength = view->ResidentSize;
file->PhysicalSize = view->ResidentSize;
if (view->ResidentSize) {
file->ResidentData = (uint8_t*)MmAllocatePool(view->ResidentSize);
if (!file->ResidentData) { ctx->Status = STATUS_NO_MEMORY; return false; }
memcpy(file->ResidentData, view->ResidentData, view->ResidentSize);
file->ResidentDataSize = view->ResidentSize;
}
} else if (attr->FormCode == NTFS_FORM_NONRESIDENT) {
file->Size = attr->Form.Nonresident.FileSize;
file->ValidDataLength = attr->Form.Nonresident.ValidDataLength;
file->PhysicalSize = attr->Form.Nonresident.AllocatedLength;
ctx->Status = NtfsReadMappings(file->Vol, attr, &file->DataRuns);
if (!KSUCCESS(ctx->Status)) return false;
}
break;
}
case NTFS_ATTR_INDEX_ROOT: {
if (NtfsAttrNameEqualsAscii(attr, "$I30") && attr->FormCode == NTFS_FORM_RESIDENT && view->ResidentSize) {
file->IndexRoot = (NTFS_INDEX_ROOT*)MmAllocatePool(view->ResidentSize);
if (!file->IndexRoot) { ctx->Status = STATUS_NO_MEMORY; return false; }
memcpy(file->IndexRoot, view->ResidentData, view->ResidentSize);
file->IndexRootSize = view->ResidentSize;
file->IsDirectory = true;
}
break;
}
case NTFS_ATTR_INDEX_ALLOCATION: {
if (NtfsAttrNameEqualsAscii(attr, "$I30") && attr->FormCode == NTFS_FORM_NONRESIDENT) {
ctx->Status = NtfsReadMappings(file->Vol, attr, &file->IndexRuns);
if (!KSUCCESS(ctx->Status)) return false;
}
break;
}
case NTFS_ATTR_ATTRIBUTE_LIST:
/* Complex attribute-list continuations are not expanded in this first native port. */
break;
default:
break;
}
return true;
}
static KSTATUS NtfsLoadFile(NTFS_VOLUME* vol, uint64_t inode, NTFS_FILE* file) {
if (!vol || !file) return STATUS_INVALID_PARAMETER;
NtfsInitFile(file, vol, inode);
void* record = MmAllocatePool((size_t)vol->FileRecordSize);
if (!record) return STATUS_NO_MEMORY;
KSTATUS s = NtfsReadRecord(vol, inode, record);
if (!KSUCCESS(s)) { MmFreePool(record); NtfsCleanupFile(file); return s; }
NTFS_LOAD_FILE_CTX ctx;
ctx.File = file;
ctx.Status = STATUS_SUCCESS;
s = NtfsForEachRecordAttribute(vol, inode, (const NTFS_FILE_RECORD_SEGMENT_HEADER*)record,
NtfsLoadFileAttributeCallback, &ctx);
MmFreePool(record);
if (!KSUCCESS(s)) { NtfsCleanupFile(file); return s; }
if (!KSUCCESS(ctx.Status)) { NtfsCleanupFile(file); return ctx.Status; }
if (file->ValidDataLength == 0 && file->ResidentDataSize) file->ValidDataLength = file->ResidentDataSize;
return STATUS_SUCCESS;
}
static KSTATUS NtfsReadFileData(NTFS_FILE* file, uint64_t offset, void* buffer, size_t size, size_t* bytesRead) {
if (bytesRead) *bytesRead = 0;
if (!file || !buffer || !bytesRead) return STATUS_INVALID_PARAMETER;
if (file->IsDirectory) return STATUS_ACCESS_DENIED;
if (offset >= file->Size || size == 0) return STATUS_SUCCESS;
uint64_t remaining = file->Size - offset;
if ((uint64_t)size > remaining) size = (size_t)remaining;
if (file->ResidentData) {
if (offset >= file->ResidentDataSize) {
memset(buffer, 0, size);
} else {
size_t avail = file->ResidentDataSize - (size_t)offset;
size_t copy = size < avail ? size : avail;
memcpy(buffer, file->ResidentData + offset, copy);
if (copy < size) memset((uint8_t*)buffer + copy, 0, size - copy);
}
*bytesRead = size;
return STATUS_SUCCESS;
}
uint64_t validEnd = offset + size;
if (validEnd > file->ValidDataLength) validEnd = file->ValidDataLength;
if (offset >= file->ValidDataLength) {
memset(buffer, 0, size);
*bytesRead = size;
return STATUS_SUCCESS;
}
uint64_t sectorSize = file->Vol->BytesPerSector;
uint64_t startAligned = offset & ~(sectorSize - 1u);
uint64_t endAligned = NtfsAlignUp(validEnd, sectorSize);
uint8_t* tmp = NULL;
uint8_t* ioBuffer = (uint8_t*)buffer;
uint64_t ioSize = endAligned - startAligned;
if (startAligned != offset || endAligned != validEnd) {
tmp = (uint8_t*)MmAllocatePool((size_t)ioSize);
if (!tmp) return STATUS_NO_MEMORY;
ioBuffer = tmp;
}
KSTATUS s = NtfsReadFromRuns(file->Vol, &file->DataRuns, startAligned, ioBuffer, ioSize);
if (!KSUCCESS(s)) { if (tmp) MmFreePool(tmp); return s; }
if (tmp) {
memcpy(buffer, tmp + (offset - startAligned), (size_t)(validEnd - offset));
MmFreePool(tmp);
}
if (validEnd < offset + size) {
memset((uint8_t*)buffer + (validEnd - offset), 0, (size_t)(offset + size - validEnd));
}
*bytesRead = size;
return STATUS_SUCCESS;
}
static uint16_t NtfsUpcaseChar(NTFS_VOLUME* vol, uint16_t c) {
if (vol && vol->Upcase) return vol->Upcase[c];
if (c >= (uint16_t)'a' && c <= (uint16_t)'z') return (uint16_t)(c - 32u);
return c;
}
static int NtfsCompareUtf16Name(NTFS_VOLUME* vol, const uint16_t* a, size_t alen, const uint16_t* b, size_t blen) {
size_t n = alen < blen ? alen : blen;
for (size_t i = 0; i < n; i++) {
uint16_t ca = NtfsUpcaseChar(vol, a[i]);
uint16_t cb = NtfsUpcaseChar(vol, b[i]);
if (ca < cb) return -1;
if (ca > cb) return 1;
}
if (alen < blen) return -1;
if (alen > blen) return 1;
return 0;
}
static size_t NtfsUtf16ToAscii(const uint16_t* src, size_t len, char* dst, size_t dstSize) {
if (!dst || dstSize == 0) return 0;
size_t out = 0;
for (size_t i = 0; i < len && out + 1 < dstSize; i++) {
uint16_t c = src[i];
dst[out++] = (c >= 0x20u && c < 0x7fu) ? (char)c : '?';
}
dst[out] = '\0';
return out;
}
static void NtfsWinTimeToSystemTime(int64_t win, SYS_SYSTEM_TIME* out) {
memset(out, 0, sizeof(*out));
if (win <= 0) return;
uint64_t unixSeconds = (uint64_t)(win / 10000000ll);
if (unixSeconds >= 11644473600ull) unixSeconds -= 11644473600ull;
else unixSeconds = 0;
out->EpochSeconds = unixSeconds;
out->Nanosecond = (uint32_t)((win % 10000000ll) * 100ll);
out->Flags = SYS_TIME_FLAG_VALID;
out->TimeZoneMinutes = SYS_TIME_UNSPECIFIED_TZ;
uint64_t days = unixSeconds / 86400ull;
uint32_t rem = (uint32_t)(unixSeconds % 86400ull);
out->Hour = (uint8_t)(rem / 3600u);
out->Minute = (uint8_t)((rem % 3600u) / 60u);
out->Second = (uint8_t)(rem % 60u);
out->DayOfWeek = (uint8_t)((days + 4u) % 7u);
int64_t z = (int64_t)days + 719468ll;
int64_t era = (z >= 0 ? z : z - 146096ll) / 146097ll;
uint32_t doe = (uint32_t)(z - era * 146097ll);
uint32_t yoe = (doe - doe / 1460u + doe / 36524u - doe / 146096u) / 365u;
int64_t y = (int64_t)yoe + era * 400ll;
uint32_t doy = doe - (365u * yoe + yoe / 4u - yoe / 100u);
uint32_t mp = (5u * doy + 2u) / 153u;
uint32_t d = doy - (153u * mp + 2u) / 5u + 1u;
uint32_t m = mp + (mp < 10u ? 3u : (uint32_t)-9);
y += (m <= 2u);
out->Year = (uint16_t)y;
out->Month = (uint8_t)m;
out->Day = (uint8_t)d;
}
static uint32_t NtfsSysAttrsFromNtfs(uint32_t attr, bool isDir) {
uint32_t out = 0;
if (isDir || (attr & NTFS_FILE_ATTRIBUTE_DIRECTORY) || (attr & NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT)) out |= SYS_FILE_ATTR_DIRECTORY;
if (attr & NTFS_FILE_ATTRIBUTE_READONLY) out |= SYS_FILE_ATTR_READ_ONLY;
if (attr & NTFS_FILE_ATTRIBUTE_HIDDEN) out |= SYS_FILE_ATTR_HIDDEN;
if (attr & NTFS_FILE_ATTRIBUTE_SYSTEM) out |= SYS_FILE_ATTR_SYSTEM;
if (attr & NTFS_FILE_ATTRIBUTE_ARCHIVE) out |= SYS_FILE_ATTR_ARCHIVE;
return out;
}
static void NtfsFillStat(NTFS_FILE* file, SYS_FILE_STAT* out) {
memset(out, 0, sizeof(*out));
out->Size = sizeof(*out);
out->Type = file->IsDirectory ? SYS_FILE_TYPE_DIRECTORY : SYS_FILE_TYPE_REGULAR;
out->Attributes = NtfsSysAttrsFromNtfs(file->Attributes, file->IsDirectory);
out->LinkCount = 1;
out->FileSize = file->Size;
out->VolumeId = file->Vol->SerialNumber;
out->FileId = file->Inode;
NtfsWinTimeToSystemTime(file->StandardInfo.CreationTime, &out->CreationTime);
NtfsWinTimeToSystemTime(file->StandardInfo.LastAccessTime, &out->LastAccessTime);
NtfsWinTimeToSystemTime(file->StandardInfo.LastWriteTime, &out->LastWriteTime);
NtfsWinTimeToSystemTime(file->StandardInfo.ChangeTime, &out->ChangeTime);
}
typedef bool (*NTFS_INDEX_ENTRY_CALLBACK)(const NTFS_FILE_NAME_ATTR* fn, uint64_t inode, void* ctx);
static KSTATUS NtfsWalkIndexBuffer(NTFS_VOLUME* vol,
NTFS_INDEX_ROOT* root,
const LIST_ENTRY* indexRuns,
const NTFS_INDEX_NODE_HEADER* nodeHeader,
const uint8_t* nodeBase,
size_t nodeBytes,
NTFS_INDEX_ENTRY_CALLBACK callback,
void* ctx,
uint32_t depth) {
if (!vol || !root || !nodeHeader || !nodeBase || !callback) return STATUS_INVALID_PARAMETER;
if (depth > 16u) return STATUS_INVALID_PARAMETER;
if (nodeHeader->FirstEntry > nodeBytes) {
NtfsTrace("NTFS: index walk: firstEntry=%u nodeBytes=%llu depth=%u\n",
nodeHeader->FirstEntry, (unsigned long long)nodeBytes, depth);
return STATUS_INVALID_PARAMETER;
}
uint32_t offset = nodeHeader->FirstEntry;
uint32_t limit = nodeHeader->TotalSize;
if (limit == 0 || limit > nodeBytes) limit = (uint32_t)nodeBytes;
while (offset + sizeof(NTFS_INDEX_ENTRY) <= limit) {
const NTFS_INDEX_ENTRY* ent = (const NTFS_INDEX_ENTRY*)(nodeBase + offset);
if (ent->EntryLength < sizeof(NTFS_INDEX_ENTRY) || offset + ent->EntryLength > limit) {
NtfsTrace("NTFS: index walk: bad entry off=%u len=%u limit=%u flags=0x%x depth=%u\n",
offset, ent->EntryLength, limit, ent->Flags, depth);
return STATUS_INVALID_PARAMETER;
}
if (!(ent->Flags & NTFS_INDEX_ENTRY_LAST) && ent->StreamLength >= offsetof(NTFS_FILE_NAME_ATTR, FileName)) {
const NTFS_FILE_NAME_ATTR* fn = (const NTFS_FILE_NAME_ATTR*)((const uint8_t*)ent + sizeof(NTFS_INDEX_ENTRY));
size_t needed = offsetof(NTFS_FILE_NAME_ATTR, FileName) + (size_t)fn->FileNameLength * sizeof(uint16_t);
if (needed <= ent->StreamLength && fn->Namespace != NTFS_FILE_NAME_DOS) {
if (!callback(fn, NtfsFileReferenceNumber(ent->FileReference), ctx)) return STATUS_SUCCESS;
}
}
if (ent->Flags & NTFS_INDEX_ENTRY_SUBNODE) {
if (ent->EntryLength < sizeof(NTFS_INDEX_ENTRY) + sizeof(uint64_t)) return STATUS_INVALID_PARAMETER;
if (!indexRuns || LIST_IS_EMPTY(indexRuns)) {
NtfsTrace("NTFS: index walk: subnode present but no $INDEX_ALLOCATION runs depth=%u off=%u\n",
depth, offset);
return STATUS_NOT_SUPPORTED;
}
uint64_t childVcn = *(const uint64_t*)((const uint8_t*)ent + ent->EntryLength - sizeof(uint64_t));
uint64_t childOffset;
if (root->BytesPerIndexRecord < vol->BytesPerCluster) childOffset = childVcn * vol->BytesPerSector;
else childOffset = childVcn * (uint64_t)vol->BytesPerCluster;
uint8_t* record = (uint8_t*)MmAllocatePool(root->BytesPerIndexRecord);
if (!record) return STATUS_NO_MEMORY;
KSTATUS s = NtfsReadFromRuns(vol, indexRuns, childOffset, record, root->BytesPerIndexRecord);
if (KSUCCESS(s)) {
NTFS_INDEX_RECORD* ir = (NTFS_INDEX_RECORD*)record;
if (ir->MultiSectorHeader.Signature != NTFS_INDEX_SIGNATURE) {
NtfsTrace("NTFS: index walk: child VCN=%llu offset=%llu signature=0x%x, expected INDX\n",
(unsigned long long)childVcn,
(unsigned long long)childOffset,
ir->MultiSectorHeader.Signature);
s = STATUS_INVALID_PARAMETER;
}
else s = NtfsProcessFixups(&ir->MultiSectorHeader, root->BytesPerIndexRecord, vol->BytesPerSector);
if (KSUCCESS(s)) {
const uint8_t* recNodeBase = (const uint8_t*)&ir->Header;
size_t recNodeBytes = root->BytesPerIndexRecord - offsetof(NTFS_INDEX_RECORD, Header);
s = NtfsWalkIndexBuffer(vol, root, indexRuns, &ir->Header, recNodeBase, recNodeBytes, callback, ctx, depth + 1u);
}
}
MmFreePool(record);
if (!KSUCCESS(s)) {
NtfsTrace("NTFS: index walk: child VCN=%llu failed st=0x%x depth=%u\n",
(unsigned long long)childVcn, s, depth);
return s;
}
}
if (ent->Flags & NTFS_INDEX_ENTRY_LAST) break;
offset += ent->EntryLength;
}
return STATUS_SUCCESS;
}
static KSTATUS NtfsWalkDirectory(NTFS_FILE* dir, NTFS_INDEX_ENTRY_CALLBACK callback, void* ctx) {
if (!dir || !dir->IsDirectory || !dir->IndexRoot || !callback) return STATUS_INVALID_PARAMETER;
if (dir->IndexRootSize < sizeof(NTFS_INDEX_ROOT)) return STATUS_INVALID_PARAMETER;
const uint8_t* nodeBase = (const uint8_t*)&dir->IndexRoot->NodeHeader;
size_t nodeBytes = dir->IndexRootSize - offsetof(NTFS_INDEX_ROOT, NodeHeader);
return NtfsWalkIndexBuffer(dir->Vol, dir->IndexRoot, &dir->IndexRuns,
&dir->IndexRoot->NodeHeader, nodeBase, nodeBytes,
callback, ctx, 0u);
}
static bool NtfsFindEntryCallback(const NTFS_FILE_NAME_ATTR* fn, uint64_t inode, void* rawCtx) {
NTFS_FIND_CTX* ctx = (NTFS_FIND_CTX*)rawCtx;
if (NtfsCompareUtf16Name(ctx->Vol, ctx->Name, ctx->NameLength, fn->FileName, fn->FileNameLength) == 0) {
ctx->Inode = inode;
ctx->Found = true;
return false;
}
return true;
}
static KSTATUS NtfsFindInDirectory(NTFS_VOLUME* vol, uint64_t dirInode, const uint16_t* name, size_t nameLength, uint64_t* outInode) {
if (!vol || !name || !nameLength || !outInode) return STATUS_INVALID_PARAMETER;
NTFS_FILE dir;
KSTATUS s = NtfsLoadFile(vol, dirInode, &dir);
if (!KSUCCESS(s)) return s;
if (!dir.IsDirectory) { NtfsCleanupFile(&dir); return STATUS_NOT_FOUND; }
NTFS_FIND_CTX ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.Vol = vol;
ctx.Name = name;
ctx.NameLength = nameLength;
s = NtfsWalkDirectory(&dir, NtfsFindEntryCallback, &ctx);
NtfsCleanupFile(&dir);
if (!KSUCCESS(s)) return s;
if (!ctx.Found) return STATUS_NOT_FOUND;
*outInode = ctx.Inode;
return STATUS_SUCCESS;
}
static KSTATUS NtfsLookupPath(NTFS_VOLUME* vol, SYS_STRING_VIEW path, uint64_t* outInode) {
if (!vol || !outInode || !SysSvIsValid(path)) return STATUS_INVALID_PARAMETER;
uint64_t inode = NTFS_ROOT_DIR_INODE;
size_t pos = 0;
while (pos < path.Length && (path.Data[pos] == '\\' || path.Data[pos] == '/')) pos++;
if (pos >= path.Length) { *outInode = inode; return STATUS_SUCCESS; }
while (pos < path.Length) {
while (pos < path.Length && (path.Data[pos] == '\\' || path.Data[pos] == '/')) pos++;
if (pos >= path.Length) break;
size_t start = pos;
while (pos < path.Length && path.Data[pos] != '\\' && path.Data[pos] != '/') pos++;
size_t len = pos - start;
if (len == 0) continue;
if (len == 1 && path.Data[start] == '.') continue;
if (len == 2 && path.Data[start] == '.' && path.Data[start + 1u] == '.') return STATUS_INVALID_PARAMETER;
if (len > 255u) return STATUS_INVALID_PARAMETER;
uint16_t segment[260];
for (size_t i = 0; i < len; i++) segment[i] = (uint16_t)(uint8_t)path.Data[start + i];
KSTATUS s = NtfsFindInDirectory(vol, inode, segment, len, &inode);
if (!KSUCCESS(s)) return s;
}
*outInode = inode;
return STATUS_SUCCESS;
}
static bool NtfsShouldHideDirectoryEntry(const NTFS_FILE_NAME_ATTR* fn, bool hideNtfsMetadata) {
if (!fn) return true;
if (fn->FileNameLength == 0) return true;
if (fn->FileNameLength == 1 && fn->FileName[0] == (uint16_t)'.') return true;
/* The NTFS root directory exposes metadata files such as $MFT, $Bitmap,
$LogFile, and $Extend. They are filesystem implementation details, not
normal user directory entries, so keep them out of directory listings.
Lookup still sees the raw index when the driver needs metadata internally. */
if (hideNtfsMetadata && fn->FileName[0] == (uint16_t)'$') return true;
return false;
}
static bool NtfsCollectEntryCallback(const NTFS_FILE_NAME_ATTR* fn, uint64_t inode, void* rawCtx) {
NTFS_COLLECT_CTX* ctx = (NTFS_COLLECT_CTX*)rawCtx;
NTFS_DIR_CTX* dir = ctx->Dir;
if (NtfsShouldHideDirectoryEntry(fn, ctx->HideNtfsMetadata)) return true;
if (dir->Count == dir->Capacity) {
uint32_t newCap = dir->Capacity ? dir->Capacity * 2u : 32u;
if (newCap < dir->Capacity) { ctx->Status = STATUS_NO_MEMORY; return false; }
NTFS_DIRENT_CACHE* newer = (NTFS_DIRENT_CACHE*)MmReallocatePool(dir->Entries, (size_t)newCap * sizeof(NTFS_DIRENT_CACHE));
if (!newer) { ctx->Status = STATUS_NO_MEMORY; return false; }
dir->Entries = newer;
dir->Capacity = newCap;
}
NTFS_DIRENT_CACHE* e = &dir->Entries[dir->Count++];
memset(e, 0, sizeof(*e));
e->Inode = inode;
e->FileSize = fn->EndOfFile;
e->AllocSize = fn->AllocationSize;
e->Attributes = fn->FileAttributes;
e->CreationTime = fn->CreationTime;
e->LastAccessTime = fn->LastAccessTime;
e->LastWriteTime = fn->LastWriteTime;
NtfsUtf16ToAscii(fn->FileName, fn->FileNameLength, e->Name, sizeof(e->Name));
ctx->Status = STATUS_SUCCESS;
return true;
}
static KSTATUS NtfsOpen(IO_FILESYSTEM* fs, SYS_STRING_VIEW path, void** fileCtx) {
if (!fs || !fileCtx || !SysSvIsValid(path)) return STATUS_INVALID_PARAMETER;
NTFS_VOLUME* vol = (NTFS_VOLUME*)fs->DriverCtx;
if (!vol) return STATUS_INVALID_PARAMETER;
uint64_t inode;
KSTATUS s = NtfsLookupPath(vol, path, &inode);
if (!KSUCCESS(s)) return s;
NTFS_FILE* file = (NTFS_FILE*)MmAllocatePool(sizeof(NTFS_FILE));
if (!file) return STATUS_NO_MEMORY;
s = NtfsLoadFile(vol, inode, file);
if (!KSUCCESS(s)) { MmFreePool(file); return s; }
*fileCtx = file;
return STATUS_SUCCESS;
}
static void NtfsClose(IO_FILESYSTEM* fs, void* fileCtx) {
(void)fs;
NTFS_FILE* file = (NTFS_FILE*)fileCtx;
if (!file) return;
NtfsCleanupFile(file);
MmFreePool(file);
}
static KSTATUS NtfsRead(IO_FILESYSTEM* fs, void* fileCtx, uint64_t offset, void* buf, size_t size, size_t* bytesRead) {
(void)fs;
return NtfsReadFileData((NTFS_FILE*)fileCtx, offset, buf, size, bytesRead);
}
static KSTATUS NtfsWrite(IO_FILESYSTEM* fs, void* fileCtx, uint64_t offset, const void* buf, size_t size, size_t* bytesWritten) {
(void)fs; (void)fileCtx; (void)offset; (void)buf; (void)size;
if (bytesWritten) *bytesWritten = 0;
return STATUS_NOT_SUPPORTED;
}
static KSTATUS NtfsCreate(IO_FILESYSTEM* fs, SYS_STRING_VIEW path, void** fileCtx) {
(void)fs; (void)path; (void)fileCtx;
return STATUS_NOT_SUPPORTED;
}
static KSTATUS NtfsDelete(IO_FILESYSTEM* fs, SYS_STRING_VIEW path) {
(void)fs; (void)path;
return STATUS_NOT_SUPPORTED;
}
static KSTATUS NtfsTruncate(IO_FILESYSTEM* fs, void* fileCtx, uint64_t newSize, uint32_t fileFlags) {
(void)fs; (void)fileCtx; (void)newSize; (void)fileFlags;
return STATUS_NOT_SUPPORTED;
}
static uint64_t NtfsGetSize(IO_FILESYSTEM* fs, void* fileCtx) {
(void)fs;
NTFS_FILE* file = (NTFS_FILE*)fileCtx;
return file ? file->Size : 0;
}
static KSTATUS NtfsQueryInfo(IO_FILESYSTEM* fs, void* fileCtx, SYS_FILE_STAT* out) {
(void)fs;
if (!fileCtx || !out) return STATUS_INVALID_PARAMETER;
NtfsFillStat((NTFS_FILE*)fileCtx, out);
return STATUS_SUCCESS;
}
static KSTATUS NtfsStat(IO_FILESYSTEM* fs, SYS_STRING_VIEW path, SYS_FILE_STAT* out) {
if (!fs || !out || !SysSvIsValid(path)) return STATUS_INVALID_PARAMETER;
void* ctx = NULL;
KSTATUS s = NtfsOpen(fs, path, &ctx);
if (!KSUCCESS(s)) return s;
s = NtfsQueryInfo(fs, ctx, out);
NtfsClose(fs, ctx);
return s;
}
static KSTATUS NtfsQueryIdentity(IO_FILESYSTEM* fs, void* fileCtx, IO_FILE_IDENTITY* out) {
(void)fs;
if (!fileCtx || !out) return STATUS_INVALID_PARAMETER;
NTFS_FILE* file = (NTFS_FILE*)fileCtx;
out->VolumeId = file->Vol->SerialNumber;
out->FileId = file->Inode;
out->Generation = (uint64_t)file->StandardInfo.LastWriteTime ^ ((uint64_t)file->StandardInfo.ChangeTime << 1u);
out->Size = file->Size;
return STATUS_SUCCESS;
}
static KSTATUS NtfsOpenDir(IO_FILESYSTEM* fs, SYS_STRING_VIEW dirPath, void** dirCtx) {
if (!fs || !dirCtx || !SysSvIsValid(dirPath)) return STATUS_INVALID_PARAMETER;
NTFS_VOLUME* vol = (NTFS_VOLUME*)fs->DriverCtx;
if (!vol) return STATUS_INVALID_PARAMETER;
uint64_t inode;
KSTATUS s = NtfsLookupPath(vol, dirPath, &inode);
if (!KSUCCESS(s)) {
NtfsTrace("NTFS: opendir: lookup failed drive=%u pathLen=%llu st=0x%x\n",
vol->VolumeIndex, (unsigned long long)dirPath.Length, s);
return s;
}
NTFS_FILE dir;
s = NtfsLoadFile(vol, inode, &dir);
if (!KSUCCESS(s)) {
NtfsTrace("NTFS: opendir: load inode=%llu failed st=0x%x\n",
(unsigned long long)inode, s);
return s;
}
if (!dir.IsDirectory) {
NtfsTrace("NTFS: opendir: inode=%llu is not directory attr=0x%x indexRoot=%p\n",
(unsigned long long)inode, dir.Attributes, dir.IndexRoot);
NtfsCleanupFile(&dir);
return STATUS_NOT_FOUND;
}
if (!dir.IndexRoot) {
NtfsTrace("NTFS: opendir: inode=%llu has no $I30 $INDEX_ROOT\n",
(unsigned long long)inode);
} else {
NtfsTrace("NTFS: opendir: inode=%llu indexRootSize=%llu indexRecordBytes=%u indexRunsEmpty=%u\n",
(unsigned long long)inode,
(unsigned long long)dir.IndexRootSize,
dir.IndexRoot->BytesPerIndexRecord,
LIST_IS_EMPTY(&dir.IndexRuns) ? 1u : 0u);
}
NTFS_DIR_CTX* ctx = (NTFS_DIR_CTX*)MmAllocatePool(sizeof(NTFS_DIR_CTX));
if (!ctx) { NtfsCleanupFile(&dir); return STATUS_NO_MEMORY; }
memset(ctx, 0, sizeof(*ctx));
NTFS_COLLECT_CTX collect;
memset(&collect, 0, sizeof(collect));
collect.Vol = vol;
collect.Dir = ctx;
collect.Status = STATUS_SUCCESS;
collect.HideNtfsMetadata = (inode == NTFS_ROOT_DIR_INODE);
s = NtfsWalkDirectory(&dir, NtfsCollectEntryCallback, &collect);
NtfsCleanupFile(&dir);
if (!KSUCCESS(s)) {
NtfsTrace("NTFS: opendir: walk inode=%llu failed st=0x%x collected=%u\n",
(unsigned long long)inode, s, ctx->Count);
if (ctx->Entries) MmFreePool(ctx->Entries);
MmFreePool(ctx);
return s;
}
if (!KSUCCESS(collect.Status)) {
NtfsTrace("NTFS: opendir: collect inode=%llu failed st=0x%x collected=%u\n",
(unsigned long long)inode, collect.Status, ctx->Count);
if (ctx->Entries) MmFreePool(ctx->Entries);
MmFreePool(ctx);
return collect.Status;
}
NtfsTrace("NTFS: opendir: inode=%llu OK entries=%u\n",
(unsigned long long)inode, ctx->Count);
*dirCtx = ctx;
return STATUS_SUCCESS;
}
static KSTATUS NtfsReadDir(IO_FILESYSTEM* fs, void* dirCtx, KDIR_ENTRY* out) {
(void)fs;
if (!dirCtx || !out) return STATUS_INVALID_PARAMETER;
NTFS_DIR_CTX* ctx = (NTFS_DIR_CTX*)dirCtx;
if (ctx->Position >= ctx->Count) return STATUS_NO_MORE_FILES;
NTFS_DIRENT_CACHE* e = &ctx->Entries[ctx->Position++];
memset(out, 0, sizeof(*out));
strncpy(out->Name, e->Name, sizeof(out->Name) - 1u);
out->Attr = (uint8_t)NtfsSysAttrsFromNtfs(e->Attributes, (e->Attributes & NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT) != 0);
out->FileSize = e->FileSize > 0xffffffffull ? 0xffffffffu : (uint32_t)e->FileSize;
NtfsWinTimeToSystemTime(e->CreationTime, &out->CreateTime);
NtfsWinTimeToSystemTime(e->LastAccessTime, &out->LastAccessTime);
NtfsWinTimeToSystemTime(e->LastWriteTime, &out->LastWriteTime);
return STATUS_SUCCESS;
}
static void NtfsCloseDir(IO_FILESYSTEM* fs, void* dirCtx) {
(void)fs;
NTFS_DIR_CTX* ctx = (NTFS_DIR_CTX*)dirCtx;
if (!ctx) return;
if (ctx->Entries) MmFreePool(ctx->Entries);
MmFreePool(ctx);
}
static void NtfsQueryStats(IO_FILESYSTEM* fs, IO_FILESYSTEM_STATS* out) {
(void)fs;
if (out) memset(out, 0, sizeof(*out));
}
static void NtfsInitVtable(IO_FILESYSTEM* fs, NTFS_VOLUME* vol) {
memset(fs, 0, sizeof(*fs));
fs->Open = NtfsOpen;
fs->Read = NtfsRead;
fs->Write = NtfsWrite;
fs->Create = NtfsCreate;
fs->Delete = NtfsDelete;
fs->Truncate = NtfsTruncate;
fs->GetSize = NtfsGetSize;
fs->QueryInfo = NtfsQueryInfo;
fs->Stat = NtfsStat;
fs->QueryIdentity = NtfsQueryIdentity;
fs->Close = NtfsClose;
fs->QueryStats = NtfsQueryStats;
fs->DriverCtx = vol;
fs->OpenDir = NtfsOpenDir;
fs->ReadDir = NtfsReadDir;
fs->CloseDir = NtfsCloseDir;
}
static bool NtfsGuidIsZero(const NTFS_GPT_GUID* guid) {
static const NTFS_GPT_GUID zeroGuid = {0};
return memcmp(guid, &zeroGuid, sizeof(*guid)) == 0;
}
static bool NtfsIsValidBootSector(const NTFS_BOOT_SECTOR* bs) {
if (!bs) return false;
if (memcmp(bs->FsName, NTFS_FS_NAME, 8) != 0) return false;
if (!NtfsIsPowerOfTwo32(bs->BytesPerSector)) return false;
if (bs->BytesPerSector != NTFS_SECTOR_SIZE) return false;
if (!NtfsIsPowerOfTwo32(bs->SectorsPerCluster)) return false;
if (bs->TotalSectors == 0 || bs->MFT == 0) return false;
return true;
}
static uint64_t NtfsDecodeRecordSize(int8_t clusters, uint32_t bytesPerCluster) {
if (clusters > 0) return (uint64_t)(uint8_t)clusters * bytesPerCluster;
if (clusters < 0 && clusters > -64) return 1ull << (uint8_t)(-clusters);
return 0;
}
typedef struct NTFS_MFT_CTX {
NTFS_VOLUME* Vol;
KSTATUS Status;
bool Found;
} NTFS_MFT_CTX;
static bool NtfsMftAttributeCallback(const NTFS_ATTR_VIEW* view, void* rawCtx) {
NTFS_MFT_CTX* ctx = (NTFS_MFT_CTX*)rawCtx;
const NTFS_ATTRIBUTE_RECORD_HEADER* attr = view->Attr;
if (attr->TypeCode == NTFS_ATTR_DATA && attr->NameLength == 0 && attr->FormCode == NTFS_FORM_NONRESIDENT) {
ctx->Status = NtfsReadMappings(ctx->Vol, attr, &ctx->Vol->MftRuns);
ctx->Found = KSUCCESS(ctx->Status);
return false;
}
return true;
}
static KSTATUS NtfsReadMftRuns(NTFS_VOLUME* vol) {
void* record = MmAllocatePool((size_t)vol->FileRecordSize);
if (!record) return STATUS_NO_MEMORY;
uint64_t mftByte = vol->MftStartCluster * vol->BytesPerCluster;
KSTATUS s = NtfsReadBytesDirect(vol, mftByte, vol->FileRecordSize, record);
if (KSUCCESS(s)) {
NTFS_FILE_RECORD_SEGMENT_HEADER* fr = (NTFS_FILE_RECORD_SEGMENT_HEADER*)record;
if (fr->MultiSectorHeader.Signature != NTFS_FILE_SIGNATURE) s = STATUS_INVALID_PARAMETER;
else s = NtfsProcessFixups(&fr->MultiSectorHeader, vol->FileRecordSize, vol->BytesPerSector);
}
if (!KSUCCESS(s)) { MmFreePool(record); return s; }
NTFS_MFT_CTX ctx;
ctx.Vol = vol;
ctx.Status = STATUS_NOT_FOUND;
ctx.Found = false;
s = NtfsForEachRecordAttribute(vol, NTFS_MFT_INODE, (const NTFS_FILE_RECORD_SEGMENT_HEADER*)record,
NtfsMftAttributeCallback, &ctx);
MmFreePool(record);
if (!KSUCCESS(s)) return s;
if (!ctx.Found) return STATUS_NOT_FOUND;
if (KSUCCESS(ctx.Status)) NtfsDebugDumpRuns("mft", &vol->MftRuns);
return ctx.Status;
}
static KSTATUS NtfsInitUpcaseIdentity(NTFS_VOLUME* vol) {
if (!vol) return STATUS_INVALID_PARAMETER;
vol->Upcase = (uint16_t*)MmAllocatePool(NTFS_UPCASE_TABLE_BYTES);
if (!vol->Upcase) return STATUS_NO_MEMORY;
for (uint32_t i = 0; i < NTFS_UPCASE_TABLE_CHARS; i++) vol->Upcase[i] = (uint16_t)i;
for (uint16_t c = (uint16_t)'a'; c <= (uint16_t)'z'; c++) vol->Upcase[c] = (uint16_t)(c - 32u);
return STATUS_SUCCESS;
}
static void NtfsCleanupVolume(NTFS_VOLUME* vol) {
if (!vol) return;
if (vol->MftRuns.Fwd) NtfsFreeRuns(&vol->MftRuns);
if (vol->Upcase) {
MmFreePool(vol->Upcase);
vol->Upcase = (uint16_t*)0;
}
}
static void NtfsTryLoadUpcase(NTFS_VOLUME* vol) {
NTFS_FILE upcase;
if (!KSUCCESS(NtfsLoadFile(vol, NTFS_UPCASE_INODE, &upcase))) return;
if (vol->Upcase && upcase.Size >= NTFS_UPCASE_TABLE_BYTES) {
size_t br = 0;
(void)NtfsReadFileData(&upcase, 0, vol->Upcase, NTFS_UPCASE_TABLE_BYTES, &br);
}
NtfsCleanupFile(&upcase);
}
static KSTATUS NtfsProbeAndPrepareVolume(NTFS_VOLUME* vol, uint8_t disk, uint64_t startLba, uint64_t sectors) {
if (!vol) return STATUS_INVALID_PARAMETER;
memset(vol, 0, sizeof(*vol));
vol->DriveIndex = disk;
vol->PartitionLba = startLba;
vol->PartitionSize = sectors;
vol->BytesPerSector = NTFS_SECTOR_SIZE;
NtfsInitRunList(&vol->MftRuns);
uint8_t sector[NTFS_SECTOR_SIZE];
memset(sector, 0, sizeof(sector));
KSTATUS s = NtfsReadSectors(vol, 0, 1, sector);
if (!KSUCCESS(s)) return s;
const NTFS_BOOT_SECTOR* bs = (const NTFS_BOOT_SECTOR*)sector;
if (!NtfsIsValidBootSector(bs)) return STATUS_NOT_SUPPORTED;
vol->BytesPerSector = bs->BytesPerSector;
vol->SectorsPerCluster = bs->SectorsPerCluster;
vol->BytesPerCluster = vol->BytesPerSector * vol->SectorsPerCluster;
vol->TotalSectors = bs->TotalSectors;
vol->PartitionSize = sectors ? sectors : bs->TotalSectors;
if (vol->PartitionSize > bs->TotalSectors) vol->PartitionSize = bs->TotalSectors;
vol->SerialNumber = bs->SerialNumber;
vol->MftStartCluster = bs->MFT;
vol->FileRecordSize = NtfsDecodeRecordSize(bs->ClustersPerMFTRecord, vol->BytesPerCluster);
if (vol->FileRecordSize == 0 || vol->FileRecordSize > 65536ull || (vol->FileRecordSize % vol->BytesPerSector) != 0)
return STATUS_INVALID_PARAMETER;
s = NtfsInitUpcaseIdentity(vol);
if (!KSUCCESS(s)) return s;
s = NtfsReadMftRuns(vol);
if (!KSUCCESS(s)) { NtfsCleanupVolume(vol); return s; }
NtfsTryLoadUpcase(vol);
return STATUS_SUCCESS;
}
static uint32_t NtfsEnumerateDiskPartitions(uint8_t disk, NTFS_PARTITION_CANDIDATE* parts, uint32_t maxParts, bool* mediaReadable) {
if (mediaReadable) *mediaReadable = false;
if (!parts || maxParts == 0) return 0;
uint8_t sector[NTFS_SECTOR_SIZE];
NTFS_VOLUME raw;
memset(&raw, 0, sizeof(raw));
raw.DriveIndex = disk;
raw.PartitionLba = 0;
raw.PartitionSize = IoDiskSectorCount(disk);
raw.BytesPerSector = NTFS_SECTOR_SIZE;
NtfsInitRunList(&raw.MftRuns);
if (!KSUCCESS(NtfsReadSectors(&raw, 0, 1, sector))) return 0;
if (mediaReadable) *mediaReadable = true;
uint32_t count = 0;
NTFS_BOOT_SECTOR* vbr = (NTFS_BOOT_SECTOR*)sector;
if (NtfsIsValidBootSector(vbr)) {
parts[count].StartLba = 0;
parts[count].SectorCount = vbr->TotalSectors;
count++;
if (count >= maxParts) return count;
}
if (sector[510] != 0x55u || sector[511] != 0xaau) return count;
NTFS_MBR_PARTITION* mbr = (NTFS_MBR_PARTITION*)(sector + 446u);
bool protectiveMbr = false;
for (uint32_t i = 0; i < 4u; i++) if (mbr[i].OsType == 0xeeu) protectiveMbr = true;
if (protectiveMbr) {
uint8_t gptSector[NTFS_SECTOR_SIZE];
if (!KSUCCESS(NtfsReadSectors(&raw, 1, 1, gptSector))) return count;
NTFS_GPT_HEADER* hdr = (NTFS_GPT_HEADER*)gptSector;
if (memcmp(hdr->Signature, "EFI PART", 8) != 0 || hdr->SizeOfPartitionEntry < sizeof(NTFS_GPT_ENTRY)) return count;
uint32_t entries = hdr->NumberOfPartitionEntries;
if (entries > 256u) entries = 256u;
uint32_t entrySize = hdr->SizeOfPartitionEntry;
uint8_t entrySector[NTFS_SECTOR_SIZE];
for (uint32_t i = 0; i < entries && count < maxParts; i++) {
uint64_t byteOff = (uint64_t)i * entrySize;
uint64_t lba = hdr->PartitionEntryLBA + byteOff / NTFS_SECTOR_SIZE;
uint32_t inSector = (uint32_t)(byteOff % NTFS_SECTOR_SIZE);
if (inSector + sizeof(NTFS_GPT_ENTRY) > NTFS_SECTOR_SIZE) continue;
if (!KSUCCESS(NtfsReadSectors(&raw, lba, 1, entrySector))) break;
NTFS_GPT_ENTRY* e = (NTFS_GPT_ENTRY*)(entrySector + inSector);
if (NtfsGuidIsZero(&e->PartitionTypeGUID)) continue;
if (e->EndingLBA < e->StartingLBA) continue;
parts[count].StartLba = e->StartingLBA;
parts[count].SectorCount = e->EndingLBA - e->StartingLBA + 1ull;
count++;
}
return count;
}
for (uint32_t i = 0; i < 4u && count < maxParts; i++) {
if (mbr[i].OsType == 0 || mbr[i].SizeInLba == 0) continue;
parts[count].StartLba = mbr[i].StartingLba;
parts[count].SectorCount = mbr[i].SizeInLba;
count++;
}
return count;
}
static int NtfsReserveFreeVolumeIndex(void) {
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
for (uint8_t i = 0; i < NTFS_MAX_VOLUMES; i++) {
if (!gVolumeMounted[i] && !gVolumeBusy[i] && !IoFilesystemPresent(i)) {
gVolumeBusy[i] = true;
KeReleaseSpinlock(&gNtfsMountLock, flags);
return (int)i;
}
}
KeReleaseSpinlock(&gNtfsMountLock, flags);
return -1;
}
static void NtfsReleaseReservedVolumeIndex(uint8_t volumeIndex) {
if (volumeIndex >= NTFS_MAX_VOLUMES) return;
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
gVolumeBusy[volumeIndex] = false;
KeReleaseSpinlock(&gNtfsMountLock, flags);
}
static bool NtfsBeginDiskScan(uint8_t disk) {
if (disk >= NTFS_MAX_VOLUMES) return false;
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
if (gDiskScanned[disk]) {
KeReleaseSpinlock(&gNtfsMountLock, flags);
return false;
}
gDiskScanned[disk] = true;
KeReleaseSpinlock(&gNtfsMountLock, flags);
return true;
}
static void NtfsEndDiskScan(uint8_t disk, bool completed) {
if (disk >= NTFS_MAX_VOLUMES) return;
if (completed) return;
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
gDiskScanned[disk] = false;
KeReleaseSpinlock(&gNtfsMountLock, flags);
}
static uint32_t NtfsTryMountDisk(uint8_t disk) {
if (disk >= NTFS_MAX_VOLUMES) return 0;
if (!IoDiskDevicePresent(disk)) return 0;
if (!NtfsBeginDiskScan(disk)) return 0;
bool scanCompleted = false;
bool mediaReadable = false;
NTFS_PARTITION_CANDIDATE parts[NTFS_MAX_PARTITIONS_PER_DISK];
uint32_t partCount = NtfsEnumerateDiskPartitions(disk, parts, NTFS_MAX_PARTITIONS_PER_DISK, &mediaReadable);
if (!mediaReadable) {
DbgPrint("NTFS: disk %u: media probe deferred; sector 0 unreadable\n", disk);
NtfsEndDiskScan(disk, false);
return 0;
}
scanCompleted = true;
uint32_t mounted = 0;
for (uint32_t p = 0; p < partCount; p++) {
int freeVolume = NtfsReserveFreeVolumeIndex();
if (freeVolume < 0) break;
uint8_t volumeIndex = (uint8_t)freeVolume;
KSTATUS s = NtfsProbeAndPrepareVolume(&gVolumes[volumeIndex], disk, parts[p].StartLba, parts[p].SectorCount);
if (!KSUCCESS(s)) {
NtfsCleanupVolume(&gVolumes[volumeIndex]);
memset(&gVolumes[volumeIndex], 0, sizeof(gVolumes[volumeIndex]));
NtfsReleaseReservedVolumeIndex(volumeIndex);
continue;
}
gVolumes[volumeIndex].VolumeIndex = volumeIndex;
NtfsInitVtable(&gFilesystems[volumeIndex], &gVolumes[volumeIndex]);
IO_VOLUME_PARAMS params;
params.DiskIndex = disk;
params.PartitionLba = parts[p].StartLba;
params.PartitionSize = gVolumes[volumeIndex].PartitionSize;
s = IoRegisterFilesystem(volumeIndex, &gFilesystems[volumeIndex], &params);
if (KSUCCESS(s)) {
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
gVolumeMounted[volumeIndex] = true;
gVolumeBusy[volumeIndex] = false;
gMountedCount++;
KeReleaseSpinlock(&gNtfsMountLock, flags);
mounted++;
DbgPrint("NTFS: disk %u part %u mounted as drive %u (lba=%llu mft_record=%llu).\n",
disk, p, volumeIndex,
(unsigned long long)parts[p].StartLba,
(unsigned long long)gVolumes[volumeIndex].FileRecordSize);
} else {
NtfsCleanupVolume(&gVolumes[volumeIndex]);
memset(&gVolumes[volumeIndex], 0, sizeof(gVolumes[volumeIndex]));
memset(&gFilesystems[volumeIndex], 0, sizeof(gFilesystems[volumeIndex]));
NtfsReleaseReservedVolumeIndex(volumeIndex);
DbgPrint("NTFS: disk %u part %u mount failed status=0x%x.\n", disk, p, s);
}
}
NtfsEndDiskScan(disk, scanCompleted);
if (partCount && !mounted && gMountedCount >= NTFS_MAX_VOLUMES)
DbgPrint("NTFS: disk %u: volume table full\n", disk);
return mounted;
}
static void NtfsUnmountDisk(uint8_t diskIndex, IO_DEVICE_CHANGE change) {
uint32_t removed = 0;
for (uint8_t i = 0; i < NTFS_MAX_VOLUMES; i++) {
if (!gVolumeMounted[i] || gVolumes[i].DriveIndex != diskIndex) continue;
(void)IoUnregisterFilesystem(i);
NtfsCleanupVolume(&gVolumes[i]);
memset(&gVolumes[i], 0, sizeof(gVolumes[i]));
memset(&gFilesystems[i], 0, sizeof(gFilesystems[i]));
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
gVolumeMounted[i] = false;
gVolumeBusy[i] = false;
if (gMountedCount) gMountedCount--;
KeReleaseSpinlock(&gNtfsMountLock, flags);
removed++;
}
if (diskIndex < NTFS_MAX_VOLUMES) {
uintptr_t flags = KeAcquireSpinlock(&gNtfsMountLock);
gDiskScanned[diskIndex] = false;
KeReleaseSpinlock(&gNtfsMountLock, flags);
}
if (removed) {
DbgPrint("NTFS: disk %u %s; unmounted %u volume(s); total=%u\n",
diskIndex,
change == IoDeviceSurpriseRemoved ? "surprise removed" : "removed",
removed,
gMountedCount);
}
}
static void NtfsDiskArrival(uint8_t driveIndex, DEVICE_OBJECT* device, void* context) {
(void)device;
(void)context;
uint32_t mounted = NtfsTryMountDisk(driveIndex);
if (mounted) DbgPrint("NTFS: hotplug scan mounted %u new volume(s); total=%u\n", mounted, gMountedCount);
}
static void NtfsDiskChange(uint8_t driveIndex, DEVICE_OBJECT* device, IO_DEVICE_CHANGE change, void* context) {
(void)device;
(void)context;
switch (change) {
case IoDeviceArrived:
NtfsDiskArrival(driveIndex, device, context);
break;
case IoDeviceSurpriseRemoved:
case IoDeviceRemoved:
NtfsUnmountDisk(driveIndex, change);
break;
case IoDeviceQueryRemove:
default:
break;
}
}
KSTATUS DriverEntry(const HAL_API* hal) {
DbgPrint("NTFS: Init\n");
if (!hal) return STATUS_INVALID_PARAMETER;
(void)hal;
memset(gVolumes, 0, sizeof(gVolumes));
memset(gFilesystems, 0, sizeof(gFilesystems));
memset(gVolumeMounted, 0, sizeof(gVolumeMounted));
memset(gVolumeBusy, 0, sizeof(gVolumeBusy));
memset(gDiskScanned, 0, sizeof(gDiskScanned));
gMountedCount = 0;
KSTATUS s = IoRegisterDiskChangeCallback(NtfsDiskChange, (void*)0);
if (!KSUCCESS(s)) DbgPrint("NTFS: disk-change callback registration failed st=0x%x\n", s);
s = IoEnumerateDiskDevices(NtfsDiskArrival, (void*)0);
if (!KSUCCESS(s)) DbgPrint("NTFS: initial disk enumeration failed st=0x%x\n", s);
if (!gMountedCount) {
DbgPrint("NTFS: no mountable NTFS volumes found.\n");
return STATUS_NOT_SUPPORTED;
}
DbgPrint("NTFS: Init complete; mounted %u volume(s).\n", gMountedCount);
return STATUS_SUCCESS;
}