Files
nt-lite/code/drivers/fs/fat/fat.c
T
maksw 73abd6f90d isolate POSIX builds from the host UCRT, wire lwext4 and ntfs-3g as ext4.sys and ntfs.sys, split the FAT driver into directory/file/async/driver/mount units, add posixsrv and dwm plus input/console/desktop syscall clients and a pile of new smoke targets, rename serial.sys to cereal.sys, name HDA/USB/lwIP/AHCI workers, harden ATA probing/reset/removal around actual hardware deadlines, absent-channel detection, diagnostics and explicit worker lifetime, redo ACPI S5 shutdown around GAS/MMIO and HW-reduced firmware, scan SSDTs for _S5 and deal with Intel SMI sleep trapping, make serial and SMP timeouts use real monotonic deadlines, add thread names and fix terminal thread teardown, scheduler and APC prepare/commit races, make advisory file locks queryable and preserve split ranges and add mutable file append state, add shared-memory pipes with kernel-assisted wait/wake, introduce capability-scoped input and display managers, split surface map rights into read/write, add explicit unmap and replace present with commit/generation/damage tracking while keeping physical presentation under the display manager, decouple native exception classes from Win32 exception numbers and translate them in kernel32, add opaque process startup data, bump the resource protocol to v3 with namespace directories, symlinks and enumeration and service control to v2 with generic READY handle exports, add the new POSIX errno/status surface and rebuild fds around posixsrv-owned shared open descriptions, descriptor-local flags, exact inheritance snapshots, dup/fcntl/record locks and shared-ring pipes, add waitpid, POSIX credentials/groups/sessions/passwd/group lookups and move sockets onto stable open descriptions, port the shells and apps to the new console/input services and surface commit API, teach init to load services.ini and start/supervise RSSRV, DWM and generic services under restricted capabilities while publishing their endpoints through RSSRV, split RSSRV into namespace, volume, display, session, input, console and resource components and make /SystemRoot a real symlink to the independently published system volume, shift the syscall table again, expand lock/resource/surface/POSIX/service/thread smoke coverage, make docs scream ASK QUESTIONS and DISCUSS at LLMs, replace MIT No Attribution with WTFPL+.
2026-09-16 10:33:20 +02:00

557 lines
20 KiB
C

// FAT allocation, cache, and metadata core shared by the filesystem operation translation units.
#include "fat_internal.h"
// FAT sector cache (fixes #1 and #5)
// 32 direct-mapped slots, indexed by (lba % FAT_CACHE_SLOTS).
// Write-back: FatWriteEntry modifies the in-cache bytes and marks the slot
// dirty. FatFlushCache() propagates dirty slots to every FAT copy at once,
// replacing the original per-entry 2x(read+write) pattern with a single
// write per dirty sector per FAT copy.
#define FAT_CACHE_SLOTS 32
typedef struct {
FAT_VOL* vol;
uint64_t lba;
uint8_t data[512];
bool valid;
bool dirty;
} FAT_CACHE_ENTRY;
static FAT_CACHE_ENTRY gFatCache[FAT_CACHE_SLOTS];
// The FAT sector cache is global and direct-mapped. mmap-backed video can fault
// file pages from multiple Bink worker threads at the same time; without
// serialization one thread can evict/reload a cache slot while another thread is
// still reading a FAT entry from that slot, corrupting cluster-chain traversal.
// Use a sleepable mutex rather than a spinlock because cache misses perform disk
// I/O. The static mutex avoids allocating an object from the paging path.
static MUTEX gFatCacheMutex;
static SPINLOCK gFatCacheMutexInitLock = SPINLOCK_INIT;
static int gFatCacheMutexReady;
static void FatCacheEnsureMutex(void) {
if (gFatCacheMutexReady)
return;
uintptr_t flags = KeAcquireSpinlock(&gFatCacheMutexInitLock);
if (!gFatCacheMutexReady) {
memset(&gFatCacheMutex, 0, sizeof(gFatCacheMutex));
ObInitializeObjectHeader(&gFatCacheMutex.Header, ObjectTypeMutex);
gFatCacheMutex.Lock = (SPINLOCK)SPINLOCK_INIT;
gFatCacheMutex.Owner = (struct THREAD*)0;
LIST_INIT(&gFatCacheMutex.WaitList);
LIST_INIT(&gFatCacheMutex.OwnerLink);
gFatCacheMutex.DonationClass = KeSchedulingNormal;
gFatCacheMutex.DonationPriority = PriorityLow;
gFatCacheMutex.HasDonation = 0;
gFatCacheMutex.Abandoned = 0;
gFatCacheMutexReady = 1;
}
KeReleaseSpinlock(&gFatCacheMutexInitLock, flags);
}
static int FatCacheAcquire(void) {
if (!KeCanWait())
return 0;
FatCacheEnsureMutex();
return KE_MUTEX_ACQUIRE_SUCCEEDED(KeAcquireMutex(&gFatCacheMutex, KE_WAIT_INFINITE));
}
static void FatCacheRelease(int held) {
if (held)
KeReleaseMutex(&gFatCacheMutex);
}
// Raw (non-cached) sector I/O — forward declarations.
static KSTATUS ReadSectorsRaw (FAT_VOL* v, uint64_t lba, uint32_t count, void* buf);
static KSTATUS WriteSectorsRaw(FAT_VOL* v, uint64_t lba, uint32_t count, const void* buf);
// Read one FAT sector through the cache.
// Caller must hold the FAT cache mutex while using the returned pointer.
static KSTATUS FatCacheRead(FAT_VOL* v, uint64_t lba, uint8_t** out) {
uint32_t slot = (uint32_t)(lba % FAT_CACHE_SLOTS);
FAT_CACHE_ENTRY* e = &gFatCache[slot];
if (e->valid && e->vol == v && e->lba == lba) {
*out = e->data;
return NTLSTATUS_SUCCESS;
}
// Evict current occupant: single-copy writeback on collision.
// The full multi-copy flush happens in FatFlushCache().
if (e->valid && e->dirty && e->vol) {
WriteSectorsRaw(e->vol, e->lba, 1, e->data);
e->dirty = false;
}
KSTATUS s = ReadSectorsRaw(v, lba, 1, e->data);
if (!KSUCCESS(s)) { e->valid = false; e->vol = (FAT_VOL*)0; return s; }
e->vol = v;
e->lba = lba;
e->valid = true;
e->dirty = false;
*out = e->data;
return NTLSTATUS_SUCCESS;
}
// Implements the fat Cache Mark Dirty operation.
static void FatCacheMarkDirty(FAT_VOL* v, uint64_t lba) {
uint32_t slot = (uint32_t)(lba % FAT_CACHE_SLOTS);
if (gFatCache[slot].valid && gFatCache[slot].vol == v && gFatCache[slot].lba == lba)
gFatCache[slot].dirty = true;
}
// Invalidates every FAT sector-cache entry.
void FatCacheInvalidate(void) {
int held = FatCacheAcquire();
for (int i = 0; i < FAT_CACHE_SLOTS; i++) {
gFatCache[i].vol = (FAT_VOL*)0;
gFatCache[i].valid = false;
gFatCache[i].dirty = false;
}
FatCacheRelease(held);
}
// Discards cache entries owned by one dead or destroyed mounted FAT instance.
void FatCacheInvalidateVolume(FAT_VOL* volume) {
if (!volume)
return;
int held = FatCacheAcquire();
for (int i = 0; i < FAT_CACHE_SLOTS; i++) {
if (gFatCache[i].vol != volume)
continue;
gFatCache[i].vol = (FAT_VOL*)0;
gFatCache[i].valid = false;
gFatCache[i].dirty = false;
}
FatCacheRelease(held);
}
// FatFlushCache — write every dirty slot to all FAT copies, clear dirty flag.
// Defined after FAT_VOL is fully declared (below).
void FatFlushCache(FAT_VOL* v);
// Raw sector I/O (bypasses FAT cache; used for file data and cache eviction).
static KSTATUS ReadSectorsRaw(FAT_VOL* v, uint64_t lba,
uint32_t count, void* buf) {
if (!v) return NTLSTATUS_INVALID_PARAMETER;
return IoReadDiskSectors(v->DriveIndex, lba, count, buf);
}
static KSTATUS WriteSectorsRaw(FAT_VOL* v, uint64_t lba,
uint32_t count, const void* buf) {
if (!v) return NTLSTATUS_INVALID_PARAMETER;
return IoWriteDiskSectors(v->DriveIndex, lba, count, buf);
}
// Convenience aliases for file-data I/O.
KSTATUS FatReadSectors (FAT_VOL* v, uint64_t lba, uint32_t n, void* b) { return ReadSectorsRaw (v, lba, n, b); }
// Updates sectors.
KSTATUS FatWriteSectors(FAT_VOL* v, uint64_t lba, uint32_t n, const void* b) { return WriteSectorsRaw(v, lba, n, b); }
const char* FatVariantName(FAT_VARIANT variant) {
switch (variant) {
case FatVariant16: return "FAT16";
case FatVariant32: return "FAT32";
case FatVariant12: return "FAT12";
default: return "FAT";
}
}
static uint32_t FatEocThreshold(FAT_VOL* v) {
return v && v->Variant == FatVariant16 ? FAT16_EOC : FAT32_EOC;
}
uint32_t FatEocMark(FAT_VOL* v) {
return v && v->Variant == FatVariant16 ? FAT16_EOC_MARK : FAT32_EOC_MARK;
}
int FatIsDataCluster(FAT_VOL* v, uint32_t cluster) {
return v && cluster >= 2u && cluster < FatEocThreshold(v);
}
uint32_t FatDirentFirstCluster(FAT_VOL* v, const DIRENT* d) {
uint32_t lo = d ? d->FirstClusterLo : 0u;
uint32_t hi = (v && v->Variant == FatVariant32 && d) ? d->FirstClusterHi : 0u;
return (hi << 16) | lo;
}
void FatSetDirentFirstCluster(FAT_VOL* v, DIRENT* d, uint32_t cluster) {
if (!d) return;
d->FirstClusterLo = (uint16_t)(cluster & 0xFFFFu);
d->FirstClusterHi = (v && v->Variant == FatVariant32) ? (uint16_t)(cluster >> 16) : 0u;
}
int FatIsFixedRoot(FAT_VOL* v, uint32_t dirCluster) {
return v && v->Variant == FatVariant16 && dirCluster == FAT_FIXED_ROOT_CLUSTER;
}
uint64_t FatClusterToLba(FAT_VOL* v, uint32_t cluster);
uint32_t FatEntry(FAT_VOL* v, uint32_t cluster);
int FatDirectoryFirstSegment(FAT_VOL* v, uint32_t dirCluster,
uint32_t* cursor, uint64_t* lba,
uint32_t* sectors) {
if (!v || !cursor || !lba || !sectors) return 0;
if (FatIsFixedRoot(v, dirCluster)) {
if (!v->RootDirSectors) return 0;
*cursor = 0;
*lba = v->RootDirStart;
*sectors = 1;
return 1;
}
if (!FatIsDataCluster(v, dirCluster)) return 0;
*cursor = dirCluster;
*lba = FatClusterToLba(v, dirCluster);
*sectors = v->SectorsPerCluster;
return 1;
}
int FatDirectoryNextSegment(FAT_VOL* v, uint32_t dirCluster,
uint32_t* cursor, uint64_t* lba,
uint32_t* sectors) {
if (!v || !cursor || !lba || !sectors) return 0;
if (FatIsFixedRoot(v, dirCluster)) {
uint32_t next = *cursor + 1u;
if (next >= v->RootDirSectors) return 0;
*cursor = next;
*lba = v->RootDirStart + next;
*sectors = 1;
return 1;
}
uint32_t next = FatEntry(v, *cursor);
if (!FatIsDataCluster(v, next)) return 0;
*cursor = next;
*lba = FatClusterToLba(v, next);
*sectors = v->SectorsPerCluster;
return 1;
}
// Implements the cluster To Lba operation.
uint64_t FatClusterToLba(FAT_VOL* v, uint32_t cluster) {
return v->DataStart + (uint64_t)(cluster - 2) * v->SectorsPerCluster;
}
// FatFlushCache — needs full FAT_VOL definition.
void FatFlushCache(FAT_VOL* v) {
uint64_t fat0Start = (uint64_t)v->PartitionLba + v->ReservedSectors;
int held = FatCacheAcquire();
for (int i = 0; i < FAT_CACHE_SLOTS; i++) {
FAT_CACHE_ENTRY* e = &gFatCache[i];
if (!e->valid || !e->dirty || e->vol != v) continue;
if (e->lba < fat0Start) continue;
uint64_t sectorIdx = e->lba - fat0Start;
if (sectorIdx >= v->FatSize) continue;
for (uint32_t f = 0; f < v->NumFats; f++) {
uint64_t lba = fat0Start + (uint64_t)f * v->FatSize + sectorIdx;
WriteSectorsRaw(v, lba, 1, e->data);
}
e->dirty = false;
}
FatCacheRelease(held);
}
// FAT read / write (through cache).
uint32_t FatEntry(FAT_VOL* v, uint32_t cluster) {
if (!v || !FatIsDataCluster(v, cluster)) return 0;
uint32_t bytesPerEntry = v->Variant == FatVariant16 ? 2u : 4u;
uint32_t fatOffset = cluster * bytesPerEntry;
uint64_t sector = (uint64_t)v->PartitionLba + v->ReservedSectors
+ fatOffset / v->BytesPerSector;
uint32_t offInSec = fatOffset % v->BytesPerSector;
int held = FatCacheAcquire();
uint8_t* sec;
KSTATUS s = FatCacheRead(v, sector, &sec);
uint32_t value = 0;
if (KSUCCESS(s)) {
if (v->Variant == FatVariant16)
value = *(uint16_t*)(sec + offInSec);
else
value = *(uint32_t*)(sec + offInSec) & 0x0FFFFFFFu;
}
FatCacheRelease(held);
return value;
}
// Updates fat Entry.
KSTATUS FatWriteEntry(FAT_VOL* v, uint32_t cluster, uint32_t value) {
if (!v || !FatIsDataCluster(v, cluster)) return NTLSTATUS_INVALID_PARAMETER;
uint32_t bytesPerEntry = v->Variant == FatVariant16 ? 2u : 4u;
uint32_t fatOffset = cluster * bytesPerEntry;
uint64_t sector = (uint64_t)v->PartitionLba + v->ReservedSectors
+ fatOffset / v->BytesPerSector;
uint32_t offInSec = fatOffset % v->BytesPerSector;
int held = FatCacheAcquire();
uint8_t* sec;
KSTATUS s = FatCacheRead(v, sector, &sec);
if (!KSUCCESS(s)) {
FatCacheRelease(held);
return s;
}
if (v->Variant == FatVariant16) {
*(uint16_t*)(sec + offInSec) = (uint16_t)(value & 0xFFFFu);
} else {
uint32_t existing = *(uint32_t*)(sec + offInSec);
*(uint32_t*)(sec + offInSec) = (existing & 0xF0000000u)
| (value & 0x0FFFFFFFu);
}
FatCacheMarkDirty(v, sector);
FatCacheRelease(held);
return NTLSTATUS_SUCCESS;
}
// Cluster allocation (fix #3: next-free hint + two-phase wrap-around scan).
uint32_t FatAllocClusterEx(FAT_VOL* v, int zeroCluster) {
if (v->TotalClusters == 0) return 0;
uint32_t total = v->TotalClusters;
uint32_t start = v->NextFreeHint;
if (start < 2 || start >= total + 2) start = 2;
for (uint32_t pass = 0; pass < 2; pass++) {
uint32_t lo = (pass == 0) ? start : 2;
uint32_t hi = (pass == 0) ? total+2 : start;
for (uint32_t c = lo; c < hi; c++) {
if (FatEntry(v, c) == FAT_FREE) {
if (!KSUCCESS(FatWriteEntry(v, c, FatEocMark(v)))) return 0;
v->NextFreeHint = c + 1;
// Normal file growth zeroes newly allocated clusters so old
// disk contents are never exposed. Swap/pagefile creation is
// the one exception: every allocated slot is overwritten before
// it can ever be read back, so skipping these writes avoids a
// slow boot-time full-pagefile zero pass.
if (zeroCluster) {
uint8_t* zero = (uint8_t*)MmAllocatePool(v->BytesPerCluster);
if (zero) {
memset(zero, 0, v->BytesPerCluster);
FatWriteSectors(v, FatClusterToLba(v, c), v->SectorsPerCluster, zero);
MmFreePool(zero);
}
}
return c;
}
}
}
return 0;
}
uint32_t FatAllocCluster(FAT_VOL* v) {
return FatAllocClusterEx(v, 1);
}
uint32_t FatAppendClusterEx(FAT_VOL* v, uint32_t lastCluster, int zeroCluster) {
uint32_t newCluster = FatAllocClusterEx(v, zeroCluster);
if (!newCluster) return 0;
if (!KSUCCESS(FatWriteEntry(v, lastCluster, newCluster))) return 0;
return newCluster;
}
// Appends cluster.
uint32_t FatAppendCluster(FAT_VOL* v, uint32_t lastCluster) {
return FatAppendClusterEx(v, lastCluster, 1);
}
// Returns chain End.
uint32_t FatGetChainEnd(FAT_VOL* v, uint32_t start, uint32_t* outCount) {
uint32_t cluster = start, prev = start, count = 0;
while (FatIsDataCluster(v, cluster)) {
prev = cluster; count++;
cluster = FatEntry(v, cluster);
}
if (outCount) *outCount = count;
return prev;
}
// Releases cluster Chain.
void FatFreeClusterChain(FAT_VOL* v, uint32_t start) {
uint32_t cluster = start;
while (FatIsDataCluster(v, cluster)) {
uint32_t next = FatEntry(v, cluster);
FatWriteEntry(v, cluster, FAT_FREE);
if (cluster < v->NextFreeHint) v->NextFreeHint = cluster;
cluster = next;
}
}
// FAT timestamp and DIRENT metadata helpers.
// Implements the fat Is Leap Year operation.
static int FatIsLeapYear(uint32_t year) {
return ((year % 4u) == 0u && (year % 100u) != 0u) || ((year % 400u) == 0u);
}
// Implements the fat Days In Month operation.
static uint32_t FatDaysInMonth(uint32_t year, uint32_t month) {
static const uint8_t days[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
if (month < 1u || month > 12u) return 31u;
if (month == 2u && FatIsLeapYear(year)) return 29u;
return days[month - 1u];
}
static uint64_t FatCalendarToEpochSeconds(uint32_t year, uint32_t month, uint32_t day,
uint32_t hour, uint32_t minute, uint32_t second) {
uint64_t days = 0;
for (uint32_t y = 1970u; y < year; y++)
days += FatIsLeapYear(y) ? 366u : 365u;
for (uint32_t m = 1u; m < month; m++)
days += FatDaysInMonth(year, m);
days += day - 1u;
return (((days * 24u) + hour) * 60u + minute) * 60u + second;
}
// Implements the fat Day Of Week operation.
static uint8_t FatDayOfWeek(uint32_t year, uint32_t month, uint32_t day) {
uint64_t epoch = FatCalendarToEpochSeconds(year, month, day, 0, 0, 0);
return (uint8_t)(((epoch / 86400u) + 4u) % 7u);
}
// Implements the fat Encode Date operation.
static uint16_t FatEncodeDate(const SYS_SYSTEM_TIME* t) {
uint32_t year = t && t->Year ? t->Year : 1980u;
uint32_t month = t && t->Month ? t->Month : 1u;
uint32_t day = t && t->Day ? t->Day : 1u;
if (year < 1980u) year = 1980u;
if (year > 2107u) year = 2107u;
if (month < 1u) month = 1u;
if (month > 12u) month = 12u;
uint32_t dim = FatDaysInMonth(year, month);
if (day < 1u) day = 1u;
if (day > dim) day = dim;
return (uint16_t)(((year - 1980u) << 9) | (month << 5) | day);
}
// Implements the fat Encode Time operation.
static uint16_t FatEncodeTime(const SYS_SYSTEM_TIME* t) {
uint32_t hour = t ? t->Hour : 0u;
uint32_t minute = t ? t->Minute : 0u;
uint32_t second = t ? t->Second : 0u;
if (hour > 23u) hour = 23u;
if (minute > 59u) minute = 59u;
if (second > 59u) second = 59u;
return (uint16_t)((hour << 11) | (minute << 5) | (second / 2u));
}
// Implements the fat Encode Create Time Tenth operation.
static uint8_t FatEncodeCreateTimeTenth(const SYS_SYSTEM_TIME* t) {
uint32_t second = t ? t->Second : 0u;
uint32_t ms = t ? t->Millisecond : 0u;
if (second > 59u) second = 59u;
if (ms > 999u) ms = 999u;
uint32_t v = ((second & 1u) * 100u) + (ms / 10u);
if (v > 199u) v = 199u;
return (uint8_t)v;
}
// Implements the fat Query Encoded Time operation.
void FatQueryEncodedTime(FAT_ENCODED_TIME* out) {
SYS_SYSTEM_TIME now;
memset(&now, 0, sizeof(now));
if (KeQuerySystemTime(&now) != NTLSTATUS_SUCCESS) {
now.Year = 1980;
now.Month = 1;
now.Day = 1;
}
out->Date = FatEncodeDate(&now);
out->Time = FatEncodeTime(&now);
out->CreateTimeTenth = FatEncodeCreateTimeTenth(&now);
}
void FatDecodeDateTime(uint16_t date, uint16_t time, uint8_t createTenth,
SYS_SYSTEM_TIME* out) {
memset(out, 0, sizeof(*out));
uint32_t year = 1980u + ((date >> 9) & 0x7Fu);
uint32_t month = (date >> 5) & 0x0Fu;
uint32_t day = date & 0x1Fu;
uint32_t hour = (time >> 11) & 0x1Fu;
uint32_t minute = (time >> 5) & 0x3Fu;
uint32_t second = (time & 0x1Fu) * 2u;
if (date == 0 || month < 1u || month > 12u) { year = 1980u; month = 1u; day = 1u; }
uint32_t dim = FatDaysInMonth(year, month);
if (day < 1u) day = 1u;
if (day > dim) day = dim;
if (hour > 23u) hour = 23u;
if (minute > 59u) minute = 59u;
if (second > 58u) second = 58u;
if (createTenth >= 100u && second < 59u) second++;
out->Year = (uint16_t)year;
out->Month = (uint8_t)month;
out->Day = (uint8_t)day;
out->Hour = (uint8_t)hour;
out->Minute = (uint8_t)minute;
out->Second = (uint8_t)second;
out->Millisecond = (uint16_t)((createTenth % 100u) * 10u);
out->DayOfWeek = FatDayOfWeek(year, month, day);
out->Flags = SYS_TIME_FLAG_VALID;
out->EpochSeconds = FatCalendarToEpochSeconds(year, month, day, hour, minute, second);
}
// Implements the fat Decode Access Date operation.
void FatDecodeAccessDate(uint16_t date, SYS_SYSTEM_TIME* out) {
FatDecodeDateTime(date, 0, 0, out);
}
KSTATUS FatDirentUpdateMetadata(FAT_VOL* v, uint64_t lba,
uint32_t offset,
const FAT_DIRENT_UPDATE* update) {
if (!v || !update || !update->Flags) return NTLSTATUS_INVALID_PARAMETER;
uint8_t* sec = (uint8_t*)MmAllocatePool(v->BytesPerSector);
if (!sec) return NTLSTATUS_NO_MEMORY;
KSTATUS s = FatReadSectors(v, lba, 1, sec);
if (KSUCCESS(s)) {
DIRENT* d = (DIRENT*)(sec + offset);
if (update->Flags & FAT_DIRENT_UPDATE_SIZE)
d->FileSize = update->Size;
if (update->Flags & FAT_DIRENT_UPDATE_FIRST_CLUSTER)
FatSetDirentFirstCluster(v, d, update->FirstCluster);
if (update->Flags & FAT_DIRENT_UPDATE_CREATE_TIME) {
d->CrtTimeTenth = update->Timestamp.CreateTimeTenth;
d->CrtTime = update->Timestamp.Time;
d->CrtDate = update->Timestamp.Date;
}
if (update->Flags & FAT_DIRENT_UPDATE_WRITE_TIME) {
d->WrtTime = update->Timestamp.Time;
d->WrtDate = update->Timestamp.Date;
}
if (update->Flags & FAT_DIRENT_UPDATE_ACCESS_DATE)
d->LastAccDate = update->Timestamp.Date;
s = FatWriteSectors(v, lba, 1, sec);
}
MmFreePool(sec);
return s;
}
KSTATUS FatDirentUpdateSize(FAT_VOL* v, uint64_t lba,
uint32_t offset, uint32_t size) {
FAT_DIRENT_UPDATE u;
memset(&u, 0, sizeof(u));
FatQueryEncodedTime(&u.Timestamp);
u.Flags = FAT_DIRENT_UPDATE_SIZE | FAT_DIRENT_UPDATE_WRITE_TIME;
u.Size = size;
return FatDirentUpdateMetadata(v, lba, offset, &u);
}
KSTATUS FatDirentUpdateFirstCluster(FAT_VOL* v, uint64_t lba,
uint32_t offset, uint32_t cluster) {
FAT_DIRENT_UPDATE u;
memset(&u, 0, sizeof(u));
u.Flags = FAT_DIRENT_UPDATE_FIRST_CLUSTER;
u.FirstCluster = cluster;
return FatDirentUpdateMetadata(v, lba, offset, &u);
}