// FAT name handling, path traversal, and directory mutation. #include #include "fat_internal.h" // 8.3 name matching / assembly. static bool MatchRaw83(const char dirName[8], const char dirExt[3], const char wantName[8], const char wantExt[3]) { return memcmp(dirName, wantName, 8) == 0 && memcmp(dirExt, wantExt, 3) == 0; } static char FatUpperChar(char c); static bool FatIsLowerChar(char c); static bool FatIsValidShortChar(char c); static void Make83NameV(SYS_STRING_VIEW src, char name8[8], char ext3[3]) { memset(name8, ' ', 8); memset(ext3, ' ', 3); if (!SysSvIsValid(src) || src.Length == 0) return; size_t baseStart = 0; size_t dot = (size_t)-1; if (src.Data[0] == '.' && src.Length > 1) { baseStart = 1; } else { for (size_t i = 0; i < src.Length; i++) if (src.Data[i] == '.') dot = i; } size_t end = (dot != (size_t)-1) ? dot : src.Length; int ni = 0; for (size_t i = baseStart; i < end && ni < 8; i++) { char c = FatUpperChar(src.Data[i]); name8[ni++] = c; } if (dot != (size_t)-1) { int ei = 0; for (size_t i = dot + 1; i < src.Length && ei < 3; i++) { char c = FatUpperChar(src.Data[i]); ext3[ei++] = c; } } } // LFN assembly / matching. static char FatUpperChar(char c) { return (c >= 'a' && c <= 'z') ? (char)(c - 32) : c; } static bool FatIsLowerChar(char c) { return c >= 'a' && c <= 'z'; } static bool FatIsValidShortChar(char c) { c = FatUpperChar(c); if (c >= 'A' && c <= 'Z') return true; if (c >= '0' && c <= '9') return true; switch (c) { case '$': case '%': case '\'': case '-': case '_': case '@': case '~': case '`': case '!': case '(': case ')': case '{': case '}': case '^': case '#': case '&': return true; default: return false; } } // True when the user spelling can refer to a normal FAT 8.3 entry. This is // intentionally structural and case-insensitive: "doom1.wad" must still open // an existing short entry named DOOM1.WAD. Leading-dot names are not 8.3. static bool FatNameIsStructurallyShort83V(SYS_STRING_VIEW name) { if (!SysSvIsValid(name) || name.Length == 0) return false; if (name.Data[0] == '.') return false; size_t dot = (size_t)-1; for (size_t i = 0; i < name.Length; i++) { char c = name.Data[i]; if (c == '.') { if (dot != (size_t)-1) return false; dot = i; } else if (!FatIsValidShortChar(c)) { return false; } } size_t baseLen = (dot != (size_t)-1) ? dot : name.Length; size_t extLen = (dot != (size_t)-1) ? (name.Length - dot - 1) : 0; return baseLen >= 1 && baseLen <= 8 && extLen <= 3; } static bool FatComponentHasLowerV(SYS_STRING_VIEW name, size_t start, size_t end) { for (size_t i = start; i < end; i++) if (FatIsLowerChar(name.Data[i])) return true; return false; } static bool FatComponentHasUpperV(SYS_STRING_VIEW name, size_t start, size_t end) { for (size_t i = start; i < end; i++) if (name.Data[i] >= 'A' && name.Data[i] <= 'Z') return true; return false; } static void FatSplitShortNameV(SYS_STRING_VIEW name, size_t* baseEnd, size_t* extStart, size_t* extEnd) { size_t dot = (size_t)-1; for (size_t i = 0; i < name.Length; i++) if (name.Data[i] == '.') dot = i; if (dot == (size_t)-1) { *baseEnd = name.Length; *extStart = name.Length; *extEnd = name.Length; } else { *baseEnd = dot; *extStart = dot + 1u; *extEnd = name.Length; } } // Creation uses LFN only when the spelling cannot be represented by a short // entry, or when mixed-case 8.3 spelling would be lost. Plain lowercase // 8.3 names are represented with FAT's NT case bits, not VFAT LFN entries. static bool FatNameNeedsLfnForCreateV(SYS_STRING_VIEW name) { if (!FatNameIsStructurallyShort83V(name)) return true; size_t baseEnd, extStart, extEnd; FatSplitShortNameV(name, &baseEnd, &extStart, &extEnd); bool baseLower = FatComponentHasLowerV(name, 0, baseEnd); bool baseUpper = FatComponentHasUpperV(name, 0, baseEnd); bool extLower = FatComponentHasLowerV(name, extStart, extEnd); bool extUpper = FatComponentHasUpperV(name, extStart, extEnd); if ((baseLower && baseUpper) || (extLower && extUpper)) return true; return false; } static uint8_t FatShortNameCaseFlagsV(SYS_STRING_VIEW name) { if (!FatNameIsStructurallyShort83V(name)) return 0; size_t baseEnd, extStart, extEnd; FatSplitShortNameV(name, &baseEnd, &extStart, &extEnd); uint8_t flags = 0; if (FatComponentHasLowerV(name, 0, baseEnd) && !FatComponentHasUpperV(name, 0, baseEnd)) flags |= FAT_NTRES_BASE_LOWER; if (extStart < extEnd && FatComponentHasLowerV(name, extStart, extEnd) && !FatComponentHasUpperV(name, extStart, extEnd)) flags |= FAT_NTRES_EXT_LOWER; return flags; } static uint8_t LfnChecksum(const char name8[8], const char ext3[3]) { uint8_t sum = 0; for (int i = 0; i < 11; i++) { uint8_t c = (uint8_t)(i < 8 ? name8[i] : ext3[i - 8]); sum = (uint8_t)(((sum & 1) ? 0x80 : 0) + (sum >> 1) + c); } return sum; } static void LfnSetChar(LFN_ENTRY* e, int idx, uint16_t value) { if (idx < 5) e->Name1[idx] = value; else if (idx < 11) e->Name2[idx - 5] = value; else e->Name3[idx - 11] = value; } static void BuildLfnEntry(LFN_ENTRY* e, const char* name, size_t nameLen, uint32_t ordinal, uint32_t lfnCount, uint8_t checksum) { memset(e, 0, sizeof(*e)); e->Order = (uint8_t)ordinal; if (ordinal == lfnCount) e->Order |= 0x40; e->Attr = ATTR_LFN; e->Type = 0; e->Checksum = checksum; e->FirstCluster = 0; size_t start = (size_t)(ordinal - 1) * 13u; for (int i = 0; i < 13; i++) { size_t pos = start + (size_t)i; uint16_t ch; if (pos < nameLen) ch = (uint8_t)name[pos]; else if (pos == nameLen) ch = 0x0000; else ch = 0xFFFF; LfnSetChar(e, i, ch); } } // Creates legacy Broken Dot83. static void MakeLegacyBrokenDot83(const char* src, char name8[8], char ext3[3]) { // This exactly models the pre-fix bug for ".doomrc": empty name + "DOO". memset(name8, ' ', 8); memset(ext3, ' ', 3); const char* dot = NULL; for (const char* p = src; *p; p++) if (*p == '.') dot = p; if (!dot) return; int ei = 0; for (const char* p = dot + 1; *p && ei < 3; p++) ext3[ei++] = FatUpperChar(*p); } static bool ShortNameExistsRaw(FAT_VOL* v, uint32_t dirCluster, const char name8[8], const char ext3[3]) { uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return true; // fail closed; caller should pick another alias uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, dirCluster, &cursor, &lba, §ors)) { MmFreePool(clusterBuf); return false; } do { if (!KSUCCESS(FatReadSectors(v, lba, sectors, clusterBuf))) break; uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; for (uint32_t i = 0; i < entries; i++) { if (dir[i].Name[0] == 0x00) { MmFreePool(clusterBuf); return false; } if ((uint8_t)dir[i].Name[0] == 0xE5 || dir[i].Attr == ATTR_LFN) continue; if (MatchRaw83(dir[i].Name, dir[i].Ext, name8, ext3)) { MmFreePool(clusterBuf); return true; } } } while (FatDirectoryNextSegment(v, dirCluster, &cursor, &lba, §ors)); MmFreePool(clusterBuf); return false; } static void FatAliasAppendChar(char* dst, int* len, int maxLen, char c) { c = FatUpperChar(c); if (!FatIsValidShortChar(c)) return; if (*len < maxLen) dst[(*len)++] = c; } // Builds alias Stem Ext V. static void BuildAliasStemExtV(SYS_STRING_VIEW src, char stem[9], char ext3[3]) { memset(stem, 0, 9); memset(ext3, ' ', 3); if (!SysSvIsValid(src)) return; size_t baseStart = 0; size_t dot = (size_t)-1; if (src.Length > 1 && src.Data[0] == '.') { // Hidden/Unix-style names like .doomrc become alias stem DOOMRC. baseStart = 1; } else { for (size_t i = 0; i < src.Length; i++) if (src.Data[i] == '.') dot = i; } int slen = 0; size_t end = (dot != (size_t)-1) ? dot : src.Length; for (size_t i = baseStart; i < end; i++) FatAliasAppendChar(stem, &slen, 8, src.Data[i]); if (slen == 0) { memcpy(stem, "FILE", 4); slen = 4; } stem[slen] = '\0'; if (dot != (size_t)-1 && dot + 1 < src.Length) { int ei = 0; for (size_t i = dot + 1; i < src.Length && ei < 3; i++) { char c = FatUpperChar(src.Data[i]); if (!FatIsValidShortChar(c)) continue; ext3[ei++] = c; } } } static void MakeAliasAttempt(const char stem[9], const char extIn[3], uint32_t attempt, char name8[8], char ext3[3]) { memset(name8, ' ', 8); memcpy(ext3, extIn, 3); char suffix[8]; int suffixLen = 0; suffix[suffixLen++] = '~'; char digits[7]; int digitLen = 0; uint32_t n = attempt; do { digits[digitLen++] = (char)('0' + (n % 10)); n /= 10; } while (n && digitLen < (int)sizeof(digits)); for (int i = digitLen - 1; i >= 0; i--) suffix[suffixLen++] = digits[i]; int stemLen = (int)strlen(stem); int keep = 8 - suffixLen; if (keep < 1) keep = 1; if (stemLen < keep) keep = stemLen; memcpy(name8, stem, keep); memcpy(name8 + keep, suffix, suffixLen); } static bool MakeUniqueShortAliasV(FAT_VOL* v, uint32_t dirCluster, SYS_STRING_VIEW longName, char name8[8], char ext3[3]) { char stem[9], extCandidate[3]; BuildAliasStemExtV(longName, stem, extCandidate); for (uint32_t attempt = 1; attempt < 1000000; attempt++) { MakeAliasAttempt(stem, extCandidate, attempt, name8, ext3); if (!ShortNameExistsRaw(v, dirCluster, name8, ext3)) return true; } return false; } // Creates legacy Broken Dot83 V. static void MakeLegacyBrokenDot83V(SYS_STRING_VIEW src, char name8[8], char ext3[3]) { // This exactly models the pre-fix bug for ".doomrc": empty name + "DOO". memset(name8, ' ', 8); memset(ext3, ' ', 3); if (!SysSvIsValid(src)) return; size_t dot = (size_t)-1; for (size_t i = 0; i < src.Length; i++) if (src.Data[i] == '.') dot = i; if (dot == (size_t)-1) return; int ei = 0; for (size_t i = dot + 1; i < src.Length && ei < 3; i++) ext3[ei++] = FatUpperChar(src.Data[i]); } static KSTATUS DeleteLegacyBrokenDotAliasV(FAT_VOL* v, uint32_t dirCluster, SYS_STRING_VIEW longName) { if (!SysSvIsValid(longName) || longName.Length < 2 || longName.Data[0] != '.') return NTLSTATUS_SUCCESS; char badName[8], badExt[3]; MakeLegacyBrokenDot83V(longName, badName, badExt); uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return NTLSTATUS_NO_MEMORY; uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, dirCluster, &cursor, &lba, §ors)) { MmFreePool(clusterBuf); return NTLSTATUS_SUCCESS; } do { KSTATUS s = FatReadSectors(v, lba, sectors, clusterBuf); if (!KSUCCESS(s)) { MmFreePool(clusterBuf); return s; } uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; for (uint32_t i = 0; i < entries; i++) { if (dir[i].Name[0] == 0x00) { MmFreePool(clusterBuf); return NTLSTATUS_SUCCESS; } if ((uint8_t)dir[i].Name[0] == 0xE5 || dir[i].Attr == ATTR_LFN) continue; if (MatchRaw83(dir[i].Name, dir[i].Ext, badName, badExt)) { int emptyBase = 1; for (int j = 0; j < 8; j++) if (dir[i].Name[j] != ' ') emptyBase = 0; if (!emptyBase) continue; // Do not free the cluster chain here. The broken entry may have // aliased existing data; deleting the directory entry is enough to // hide it from hosts and avoids freeing clusters still owned by a // valid file. dir[i].Name[0] = (char)0xE5; s = FatWriteSectors(v, lba, sectors, clusterBuf); MmFreePool(clusterBuf); return s; } } } while (FatDirectoryNextSegment(v, dirCluster, &cursor, &lba, §ors)); MmFreePool(clusterBuf); return NTLSTATUS_SUCCESS; } void FatLfnAppend(char* lfn, const LFN_ENTRY* e) { int seq = (e->Order & 0x3F) - 1; if (seq < 0 || seq >= (FAT_MAX_LFN / 13)) return; int pos = seq * 13; uint16_t chars[13]; int idx = 0; for (int i = 0; i < 5; i++) chars[idx++] = e->Name1[i]; for (int i = 0; i < 6; i++) chars[idx++] = e->Name2[i]; for (int i = 0; i < 2; i++) chars[idx++] = e->Name3[i]; for (int i = 0; i < 13 && pos < FAT_MAX_LFN-1; i++, pos++) { uint16_t c = chars[i]; if (c == 0x0000 || c == 0xFFFF) { lfn[pos] = '\0'; break; } lfn[pos] = (char)(c & 0x7F); } } // Directory search and view-native path traversal. static bool FatIsPathSep(char c) { return c == '\\' || c == '/'; } // Tests whether a non-empty FAT path view points at plausible kernel-owned storage. static bool FatStringViewDataIsPlausible(SYS_STRING_VIEW view) { if (!SysSvIsValid(view)) return false; if (view.Length == 0) return true; uintptr_t data = (uintptr_t)view.Data; return data >= 0x10000u; } static bool FatNameViewIsValid(SYS_STRING_VIEW name) { if (!FatStringViewDataIsPlausible(name)) return false; if (name.Length == 0 || name.Length >= FAT_MAX_LFN) return false; for (size_t i = 0; i < name.Length; i++) { char c = name.Data[i]; if (c == '\0' || FatIsPathSep(c)) return false; } return true; } static bool LfnMatchV(const char* lfn, SYS_STRING_VIEW want) { if (!lfn || !SysSvIsValid(want)) return false; size_t i = 0; while (lfn[i] && i < want.Length) { char a = lfn[i], b = want.Data[i]; if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) return false; i++; } return lfn[i] == '\0' && i == want.Length; } static bool MatchName83V(const char* dirName, const char* dirExt, SYS_STRING_VIEW want) { char buf[13]; int n = 0; for (int i = 0; i < 8 && dirName[i] != ' '; i++) buf[n++] = (char)(dirName[i] >= 'A' && dirName[i] <= 'Z' ? dirName[i]+32 : dirName[i]); if (dirExt[0] != ' ') { buf[n++] = '.'; for (int i = 0; i < 3 && dirExt[i] != ' '; i++) buf[n++] = (char)(dirExt[i] >= 'A' && dirExt[i] <= 'Z' ? dirExt[i]+32 : dirExt[i]); } if ((size_t)n != want.Length) return false; for (int i = 0; i < n; i++) { char a = buf[i], b = want.Data[i]; if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) return false; } return true; } // Returns the path cache canonical byte used for hashes and comparisons. static char FatPathCacheChar(char c) { if (c == '/') return '\\'; if (c >= 'A' && c <= 'Z') return (char)(c + 32); return c; } // Hashes a volume-relative path using the same case/separator rules as lookup. static uint32_t FatPathCacheHash(SYS_STRING_VIEW path) { uint32_t h = 2166136261u; for (size_t i = 0; i < path.Length; i++) { h ^= (uint8_t)FatPathCacheChar(path.Data[i]); h *= 16777619u; } return h ? h : 1u; } // Tests whether the cached path names the same FAT path as the caller's view. static bool FatPathCachePathEquals(const FAT_PATH_CACHE_ENTRY* e, SYS_STRING_VIEW path) { if (!e || e->PathLength != path.Length) return false; for (size_t i = 0; i < path.Length; i++) { if (FatPathCacheChar(e->Path[i]) != FatPathCacheChar(path.Data[i])) return false; } return true; } // Adds directory search counters without holding the lock during disk I/O. static void FatPathCacheAddDirectoryStats(FAT_VOL* v, uint64_t clusters, uint64_t entries) { if (!v) return; uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); v->Stats.DirectorySearches++; v->Stats.DirectoryBlocksRead += clusters; v->Stats.DirectoryEntriesScanned += entries; KeReleaseSpinlock(&v->PathCacheLock, flags); } // Looks up a positive path-cache entry for a full volume-relative path. static bool FatPathCacheLookup(FAT_VOL* v, SYS_STRING_VIEW path, uint32_t* outCluster, uint32_t* outSize, uint8_t* outAttr, uint64_t* outDirentLba, uint32_t* outDirentOffset) { if (!v || path.Length == 0 || path.Length >= FAT_PATH_CACHE_MAX) return false; uint32_t hash = FatPathCacheHash(path); uint32_t slot = hash % FAT_PATH_CACHE_SLOTS; bool hit = false; uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); v->Stats.PathLookups++; FAT_PATH_CACHE_ENTRY* e = &v->PathCache[slot]; if (e->Valid && e->Generation == v->PathCacheGeneration && e->Hash == hash && FatPathCachePathEquals(e, path)) { if (outCluster) *outCluster = e->Cluster; if (outSize) *outSize = e->Size; if (outAttr) *outAttr = e->Attr; if (outDirentLba) *outDirentLba = e->DirentLba; if (outDirentOffset) *outDirentOffset = e->DirentOffset; v->Stats.PathCacheHits++; hit = true; } else { v->Stats.PathCacheMisses++; } KeReleaseSpinlock(&v->PathCacheLock, flags); return hit; } // Stores a positive path lookup in the volume-local path cache. static void FatPathCacheStore(FAT_VOL* v, SYS_STRING_VIEW path, uint32_t cluster, uint32_t size, uint8_t attr, uint64_t direntLba, uint32_t direntOffset) { if (!v || path.Length == 0 || path.Length >= FAT_PATH_CACHE_MAX) return; uint32_t hash = FatPathCacheHash(path); uint32_t slot = hash % FAT_PATH_CACHE_SLOTS; uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); FAT_PATH_CACHE_ENTRY* e = &v->PathCache[slot]; memset(e, 0, sizeof(*e)); e->Valid = true; e->Generation = v->PathCacheGeneration; e->Hash = hash; e->PathLength = path.Length; memcpy(e->Path, path.Data, path.Length); e->Path[path.Length] = '\0'; e->Cluster = cluster; e->Size = size; e->Attr = attr; e->DirentLba = direntLba; e->DirentOffset = direntOffset; v->Stats.PathCacheStores++; KeReleaseSpinlock(&v->PathCacheLock, flags); } // Invalidates every positive path-cache entry after a directory or size mutation. void FatPathCacheInvalidate(FAT_VOL* v) { if (!v) return; uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); v->PathCacheGeneration++; if (v->PathCacheGeneration == 0) v->PathCacheGeneration = 1; v->Stats.PathCacheInvalidations++; v->Stats.PathCacheGeneration = v->PathCacheGeneration; for (uint32_t i = 0; i < FAT_PATH_CACHE_SLOTS; i++) v->PathCache[i].Valid = false; KeReleaseSpinlock(&v->PathCacheLock, flags); } // Returns the current filesystem cache and traversal counters. void FatQueryStats(IO_VOLUME_PROVIDER* fs, IO_FILESYSTEM_STATS* out) { if (!fs || !out) return; FAT_VOL* v = (FAT_VOL*)fs->Context; if (!v) { memset(out, 0, sizeof(*out)); return; } uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); *out = v->Stats; out->PathCacheGeneration = v->PathCacheGeneration; KeReleaseSpinlock(&v->PathCacheLock, flags); } // Records one data read span and its physically contiguous cluster merges. void FatStatsAddReadRun(FAT_VOL* v, uint32_t sectors, uint32_t merges) { if (!v || !sectors) return; uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); v->Stats.DataReadRuns++; v->Stats.DataReadSectors += sectors; v->Stats.DataReadRunMerges += merges; if (v->Stats.MaxDataReadSpanSectors < sectors) v->Stats.MaxDataReadSpanSectors = sectors; KeReleaseSpinlock(&v->PathCacheLock, flags); } // Initializes the per-open cluster-chain cache for a FAT file object. void FatFileInitChainCache(FAT_FILE* f) { if (!f) return; f->ChainCacheLock = (SPINLOCK)SPINLOCK_INIT; f->ChainCacheClock = 0; memset(f->ChainCache, 0, sizeof(f->ChainCache)); } // Invalidates cached FAT extents after the file's cluster chain changes. void FatFileInvalidateChainCache(FAT_FILE* f) { if (!f) return; uintptr_t flags = KeAcquireSpinlock(&f->ChainCacheLock); memset(f->ChainCache, 0, sizeof(f->ChainCache)); f->ChainCacheClock = 0; KeReleaseSpinlock(&f->ChainCacheLock, flags); } // Locates the best cached starting point for a logical cluster index. static bool FatChainCacheLookupStart(FAT_FILE* f, uint32_t logicalIndex, uint32_t* outLogical, uint32_t* outCluster) { if (!f || !outLogical || !outCluster) return false; bool hit = false; uint32_t bestLogical = 0; uint32_t bestCluster = 0; uintptr_t flags = KeAcquireSpinlock(&f->ChainCacheLock); for (uint32_t i = 0; i < FAT_CHAIN_CACHE_SLOTS; i++) { FAT_CHAIN_CACHE_ENTRY* e = &f->ChainCache[i]; if (!e->Valid || e->Length == 0) continue; uint32_t end = e->LogicalStart + e->Length; if (logicalIndex >= e->LogicalStart && logicalIndex < end) { *outLogical = logicalIndex; *outCluster = e->PhysicalStart + (logicalIndex - e->LogicalStart); hit = true; break; } if (end <= logicalIndex && FatIsDataCluster(f->Vol, e->NextCluster)) { if (!hit || end > bestLogical) { bestLogical = end; bestCluster = e->NextCluster; hit = true; } } } if (hit && bestCluster) { *outLogical = bestLogical; *outCluster = bestCluster; } KeReleaseSpinlock(&f->ChainCacheLock, flags); return hit; } // Stores a physically contiguous FAT extent in the per-open chain cache. static void FatChainCacheStore(FAT_FILE* f, uint32_t logicalStart, uint32_t physicalStart, uint32_t length, uint32_t nextCluster) { if (!f || !FatIsDataCluster(f->Vol, physicalStart) || length == 0) return; uintptr_t flags = KeAcquireSpinlock(&f->ChainCacheLock); uint32_t slot = f->ChainCacheClock++ % FAT_CHAIN_CACHE_SLOTS; FAT_CHAIN_CACHE_ENTRY* e = &f->ChainCache[slot]; e->Valid = true; e->LogicalStart = logicalStart; e->PhysicalStart = physicalStart; e->Length = length; e->NextCluster = nextCluster; KeReleaseSpinlock(&f->ChainCacheLock, flags); } // Resolves a logical file cluster through the cached FAT-chain extent map. uint32_t FatResolveFileCluster(FAT_FILE* f, uint32_t logicalIndex) { if (!f || !FatIsDataCluster(f->Vol, f->StartCluster)) return 0; if (logicalIndex == 0) return f->StartCluster; uint32_t currentLogical = 0; uint32_t cluster = f->StartCluster; if (!FatChainCacheLookupStart(f, logicalIndex, ¤tLogical, &cluster)) { currentLogical = 0; cluster = f->StartCluster; } while (currentLogical < logicalIndex && FatIsDataCluster(f->Vol, cluster)) { cluster = FatEntry(f->Vol, cluster); currentLogical++; } return cluster; } // Builds and caches one physically contiguous read run from a starting cluster. bool FatBuildReadRun(FAT_FILE* f, uint32_t logicalIndex, uint32_t startCluster, uint32_t maxSectors, uint32_t* outRunSectors, uint32_t* outNextCluster) { if (!f || !outRunSectors || !outNextCluster) return false; FAT_VOL* v = f->Vol; if (!v || !FatIsDataCluster(v, startCluster)) return false; uint32_t runSectors = v->SectorsPerCluster; uint32_t runClusters = 1; uint32_t last = startCluster; uint32_t next = FatEntry(v, last); while (next == last + 1 && FatIsDataCluster(v, next) && runSectors + v->SectorsPerCluster <= maxSectors) { last = next; runClusters++; runSectors += v->SectorsPerCluster; next = FatEntry(v, last); } FatChainCacheStore(f, logicalIndex, startCluster, runClusters, next); *outRunSectors = runSectors; *outNextCluster = next; return true; } static bool SearchDirV(FAT_VOL* v, uint32_t dirCluster, SYS_STRING_VIEW name, uint32_t* outCluster, uint32_t* outSize, uint8_t* outAttr, uint64_t* outDirentLba, uint32_t* outDirentOffset) { if (!FatNameViewIsValid(name)) return false; uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return false; char lfn[FAT_MAX_LFN]; bool hasLfn = false; memset(lfn, 0, sizeof(lfn)); char wantName83[8], wantExt83[3]; bool allowShortMatch = FatNameIsStructurallyShort83V(name); Make83NameV(name, wantName83, wantExt83); uint64_t clustersRead = 0; uint64_t entriesScanned = 0; bool found = false; uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, dirCluster, &cursor, &lba, §ors)) goto done; do { if (!KSUCCESS(FatReadSectors(v, lba, sectors, clusterBuf))) break; clustersRead++; uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; for (uint32_t i = 0; i < entries; i++) { entriesScanned++; if (dir[i].Name[0] == 0x00) goto done; if ((uint8_t)dir[i].Name[0] == 0xE5) { hasLfn = false; memset(lfn, 0, sizeof(lfn)); continue; } if (dir[i].Attr == ATTR_LFN) { FatLfnAppend(lfn, (LFN_ENTRY*)&dir[i]); hasLfn = true; continue; } bool match = false; if (hasLfn && lfn[0]) match = LfnMatchV(lfn, name); if (!match && allowShortMatch) match = MatchName83V(dir[i].Name, dir[i].Ext, name); if (!match && allowShortMatch) match = MatchRaw83(dir[i].Name, dir[i].Ext, wantName83, wantExt83); hasLfn = false; memset(lfn, 0, sizeof(lfn)); if (match) { *outCluster = FatDirentFirstCluster(v, &dir[i]); *outSize = dir[i].FileSize; *outAttr = dir[i].Attr; if (outDirentLba || outDirentOffset) { uint64_t eb = (uint64_t)i * sizeof(DIRENT); if (outDirentLba) *outDirentLba = lba + eb / v->BytesPerSector; if (outDirentOffset) *outDirentOffset = (uint32_t)(eb % v->BytesPerSector); } found = true; goto done; } } } while (FatDirectoryNextSegment(v, dirCluster, &cursor, &lba, §ors)); done: FatPathCacheAddDirectoryStats(v, clustersRead, entriesScanned); MmFreePool(clusterBuf); return found; } static bool FatPathNextComponent(SYS_STRING_VIEW path, size_t* cursor, SYS_STRING_VIEW* component) { if (!cursor || !component || !SysSvIsValid(path)) return false; size_t pos = *cursor; while (pos < path.Length && FatIsPathSep(path.Data[pos])) pos++; if (pos >= path.Length) { *cursor = pos; *component = SYS_SV_NULL; return false; } size_t start = pos; while (pos < path.Length && !FatIsPathSep(path.Data[pos])) pos++; *cursor = pos; *component = SysSvFromParts(path.Data + start, pos - start); return true; } KSTATUS FatLookupPathV(FAT_VOL* v, SYS_STRING_VIEW path, uint32_t* outCluster, uint32_t* outSize, uint8_t* outAttr, uint64_t* outDirentLba, uint32_t* outDirentOffset) { if (!v || !outCluster || !outSize || !FatStringViewDataIsPlausible(path)) return NTLSTATUS_INVALID_PARAMETER; if (SysSvContainsByte(path, '\0')) return NTLSTATUS_INVALID_PARAMETER; uint32_t cluster = v->RootCluster, size = 0; uint8_t attr = ATTR_DIRECTORY; uint64_t finalDirentLba = 0; uint32_t finalDirentOffset = 0; if (outDirentLba) *outDirentLba = 0; if (outDirentOffset) *outDirentOffset = 0; if (FatPathCacheLookup(v, path, outCluster, outSize, &attr, outDirentLba, outDirentOffset)) { if (outAttr) *outAttr = attr; return NTLSTATUS_SUCCESS; } size_t cursor = 0; SYS_STRING_VIEW component; while (FatPathNextComponent(path, &cursor, &component)) { if (component.Length >= FAT_MAX_LFN) return NTLSTATUS_BUFFER_TOO_SMALL; if (!FatNameViewIsValid(component)) return NTLSTATUS_INVALID_PARAMETER; if (!(attr & ATTR_DIRECTORY)) return NTLSTATUS_NOT_FOUND; uint32_t childCluster, childSize; uint8_t childAttr; uint64_t childLba = 0; uint32_t childOffset = 0; if (!SearchDirV(v, cluster, component, &childCluster, &childSize, &childAttr, &childLba, &childOffset)) return NTLSTATUS_NOT_FOUND; cluster = childCluster; size = childSize; attr = childAttr; finalDirentLba = childLba; finalDirentOffset = childOffset; if (outDirentLba) *outDirentLba = childLba; if (outDirentOffset) *outDirentOffset = childOffset; } *outCluster = cluster; *outSize = size; if (outAttr) *outAttr = attr; FatPathCacheStore(v, path, cluster, size, attr, finalDirentLba, finalDirentOffset); return NTLSTATUS_SUCCESS; } KSTATUS FatTraverseV(FAT_VOL* v, SYS_STRING_VIEW path, uint32_t* outCluster, uint32_t* outSize) { return FatLookupPathV(v, path, outCluster, outSize, NULL, NULL, NULL); } static KSTATUS SplitPathV(SYS_STRING_VIEW path, SYS_STRING_VIEW* parent, SYS_STRING_VIEW* name) { if (!parent || !name || !SysSvIsValid(path)) return NTLSTATUS_INVALID_PARAMETER; *parent = SYS_SV_NULL; *name = SYS_SV_NULL; while (path.Length && FatIsPathSep(path.Data[path.Length - 1])) path.Length--; if (path.Length == 0) return NTLSTATUS_INVALID_PARAMETER; if (SysSvContainsByte(path, '\0')) return NTLSTATUS_INVALID_PARAMETER; size_t lastSep = (size_t)-1; for (size_t i = 0; i < path.Length; i++) { if (FatIsPathSep(path.Data[i])) lastSep = i; } if (lastSep == (size_t)-1) { *parent = SYS_SV_NULL; *name = path; } else { *parent = SysSvFromParts(path.Data, lastSep); *name = SysSvFromParts(path.Data + lastSep + 1, path.Length - lastSep - 1); } if (!FatNameViewIsValid(*name)) return NTLSTATUS_INVALID_PARAMETER; return NTLSTATUS_SUCCESS; } // Find a free DIRENT slot. static bool FindFreeDirent(FAT_VOL* v, uint32_t dirCluster, uint64_t* outLba, uint32_t* outOffset) { uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return false; uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, dirCluster, &cursor, &lba, §ors)) { MmFreePool(clusterBuf); return false; } do { if (!KSUCCESS(FatReadSectors(v, lba, sectors, clusterBuf))) { MmFreePool(clusterBuf); return false; } uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; for (uint32_t i = 0; i < entries; i++) { uint8_t first = (uint8_t)dir[i].Name[0]; if (first == 0xE5 || first == 0x00) { uint64_t eb = (uint64_t)i * sizeof(DIRENT); *outLba = lba + eb / v->BytesPerSector; *outOffset = (uint32_t)(eb % v->BytesPerSector); MmFreePool(clusterBuf); return true; } } } while (FatDirectoryNextSegment(v, dirCluster, &cursor, &lba, §ors)); if (FatIsFixedRoot(v, dirCluster)) { MmFreePool(clusterBuf); return false; } uint32_t newCluster = FatAppendCluster(v, cursor); if (newCluster) { *outLba = FatClusterToLba(v, newCluster); *outOffset = 0; MmFreePool(clusterBuf); return true; } MmFreePool(clusterBuf); return false; } // Find a contiguous run of free directory entries. // VFAT LFN creation requires N long-name entries to immediately precede the // final short entry, so the run must live inside one readable/writeable segment. static bool FindFreeDirentRun(FAT_VOL* v, uint32_t dirCluster, uint32_t needed, uint64_t* outLba, uint32_t* outFirstIndex, uint32_t* outSectors) { if (!needed) return false; uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return false; uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, dirCluster, &cursor, &lba, §ors)) { MmFreePool(clusterBuf); return false; } do { if (!KSUCCESS(FatReadSectors(v, lba, sectors, clusterBuf))) { MmFreePool(clusterBuf); return false; } uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; uint32_t runStart = 0, runLen = 0; for (uint32_t i = 0; i < entries; i++) { uint8_t first = (uint8_t)dir[i].Name[0]; int freeEntry = (first == 0xE5 || first == 0x00); if (freeEntry) { if (runLen == 0) runStart = i; runLen++; if (runLen >= needed) { *outLba = lba; *outFirstIndex = runStart; *outSectors = sectors; MmFreePool(clusterBuf); return true; } } else { runLen = 0; } } } while (FatDirectoryNextSegment(v, dirCluster, &cursor, &lba, §ors)); if (FatIsFixedRoot(v, dirCluster)) { MmFreePool(clusterBuf); return false; } uint32_t newCluster = FatAppendCluster(v, cursor); if (newCluster) { *outLba = FatClusterToLba(v, newCluster); *outFirstIndex = 0; *outSectors = v->SectorsPerCluster; MmFreePool(clusterBuf); return true; } MmFreePool(clusterBuf); return false; } // Finds the parent directory and final component for a create or delete path. KSTATUS FatResolveParentForMutation(FAT_VOL* v, SYS_STRING_VIEW path, uint32_t* parentClusterOut, SYS_STRING_VIEW* filenameOut) { if (!v || !parentClusterOut || !filenameOut) return NTLSTATUS_INVALID_PARAMETER; SYS_STRING_VIEW parentPath; SYS_STRING_VIEW filenameView; KSTATUS s = SplitPathV(path, &parentPath, &filenameView); if (!KSUCCESS(s)) return s; uint32_t parentCluster = 0; uint32_t parentSize = 0; uint8_t parentAttr = 0; s = FatLookupPathV(v, parentPath, &parentCluster, &parentSize, &parentAttr, NULL, NULL); if (!KSUCCESS(s)) return s; if (!(parentAttr & ATTR_DIRECTORY)) return NTLSTATUS_NOT_FOUND; *parentClusterOut = parentCluster; *filenameOut = filenameView; return NTLSTATUS_SUCCESS; } // Creates a FAT directory entry run for a new file or directory. KSTATUS FatCreateDirent(FAT_VOL* v, SYS_STRING_VIEW path, uint8_t attr, uint32_t firstCluster, uint32_t fileSize, uint64_t* direntLbaOut, uint32_t* direntOffsetOut) { if (!v || !SysSvIsValid(path) || path.Length == 0) return NTLSTATUS_INVALID_PARAMETER; uint32_t parentCluster = 0; SYS_STRING_VIEW filenameView; KSTATUS s = FatResolveParentForMutation(v, path, &parentCluster, &filenameView); if (!KSUCCESS(s)) return s; { uint32_t ec, es; uint8_t ea; uint64_t existingLba = 0; uint32_t existingOffset = 0; if (SearchDirV(v, parentCluster, filenameView, &ec, &es, &ea, &existingLba, &existingOffset)) { return NTLSTATUS_OBJECT_NAME_COLLISION; } } bool useLfn = FatNameNeedsLfnForCreateV(filenameView); char shortName[8], shortExt[3]; if (useLfn) { s = DeleteLegacyBrokenDotAliasV(v, parentCluster, filenameView); if (!KSUCCESS(s)) return s; if (!MakeUniqueShortAliasV(v, parentCluster, filenameView, shortName, shortExt)) return NTLSTATUS_OBJECT_NAME_COLLISION; } else { Make83NameV(filenameView, shortName, shortExt); } size_t nameLen = filenameView.Length; if (nameLen == 0 || nameLen >= FAT_MAX_LFN) return NTLSTATUS_INVALID_PARAMETER; uint32_t lfnCount = useLfn ? (uint32_t)((nameLen + 12u) / 13u) : 0; uint32_t neededEntries = lfnCount + 1u; uint64_t clusterLba; uint32_t firstIndex; uint32_t entrySectors; if (!FindFreeDirentRun(v, parentCluster, neededEntries, &clusterLba, &firstIndex, &entrySectors)) return NTLSTATUS_NO_MEMORY; uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return NTLSTATUS_NO_MEMORY; s = FatReadSectors(v, clusterLba, entrySectors, clusterBuf); if (!KSUCCESS(s)) { MmFreePool(clusterBuf); return s; } DIRENT* dir = (DIRENT*)clusterBuf; uint32_t shortIndex = firstIndex + lfnCount; if (useLfn) { uint8_t checksum = LfnChecksum(shortName, shortExt); for (uint32_t slot = 0; slot < lfnCount; slot++) { uint32_t ordinal = lfnCount - slot; BuildLfnEntry((LFN_ENTRY*)&dir[firstIndex + slot], filenameView.Data, nameLen, ordinal, lfnCount, checksum); } } DIRENT* d = &dir[shortIndex]; memset(d, 0, sizeof(DIRENT)); memcpy(d->Name, shortName, 8); memcpy(d->Ext, shortExt, 3); d->Attr = attr; d->Reserved = FatShortNameCaseFlagsV(filenameView); FatSetDirentFirstCluster(v, d, firstCluster); d->FileSize = fileSize; { FAT_ENCODED_TIME now; FatQueryEncodedTime(&now); d->CrtTimeTenth = now.CreateTimeTenth; d->CrtTime = now.Time; d->CrtDate = now.Date; d->LastAccDate = now.Date; d->WrtTime = now.Time; d->WrtDate = now.Date; } s = FatWriteSectors(v, clusterLba, entrySectors, clusterBuf); MmFreePool(clusterBuf); if (!KSUCCESS(s)) return s; uint64_t shortEntryByte = (uint64_t)shortIndex * sizeof(DIRENT); uint64_t direntLba = clusterLba + shortEntryByte / v->BytesPerSector; uint32_t direntOffset = (uint32_t)(shortEntryByte % v->BytesPerSector); if (direntLbaOut) *direntLbaOut = direntLba; if (direntOffsetOut) *direntOffsetOut = direntOffset; FatPathCacheInvalidate(v); return NTLSTATUS_SUCCESS; } // Initializes one dot entry in a new directory cluster. static void FatInitDotEntry(FAT_VOL* v, DIRENT* d, const char* name, uint32_t firstCluster, const FAT_ENCODED_TIME* now) { memset(d, 0, sizeof(*d)); memset(d->Name, ' ', sizeof(d->Name)); memset(d->Ext, ' ', sizeof(d->Ext)); d->Name[0] = name[0]; if (name[1]) d->Name[1] = name[1]; d->Attr = ATTR_DIRECTORY; FatSetDirentFirstCluster(v, d, firstCluster); d->CrtTimeTenth = now->CreateTimeTenth; d->CrtTime = now->Time; d->CrtDate = now->Date; d->LastAccDate = now->Date; d->WrtTime = now->Time; d->WrtDate = now->Date; } // Writes the initial contents of a newly allocated FAT directory cluster. KSTATUS FatInitializeDirectoryCluster(FAT_VOL* v, uint32_t dirCluster, uint32_t parentCluster) { if (!v || dirCluster < 2) return NTLSTATUS_INVALID_PARAMETER; uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return NTLSTATUS_NO_MEMORY; memset(clusterBuf, 0, v->BytesPerCluster); FAT_ENCODED_TIME now; FatQueryEncodedTime(&now); DIRENT* dir = (DIRENT*)clusterBuf; FatInitDotEntry(v, &dir[0], ".", dirCluster, &now); FatInitDotEntry(v, &dir[1], "..", FatIsFixedRoot(v, parentCluster) ? 0u : parentCluster, &now); KSTATUS s = FatWriteSectors(v, FatClusterToLba(v, dirCluster), v->SectorsPerCluster, clusterBuf); MmFreePool(clusterBuf); return s; } // Tests whether a FAT directory entry is one of the synthetic dot entries. static bool FatIsDotEntry(const DIRENT* d) { if (!d || !(d->Attr & ATTR_DIRECTORY)) return false; if (d->Name[0] != '.') return false; if (d->Name[1] == ' ') return true; return d->Name[1] == '.' && d->Name[2] == ' '; } // Tests whether a directory contains any real child entries. KSTATUS FatDirectoryIsEmpty(FAT_VOL* v, uint32_t dirCluster, bool* emptyOut) { if (!v || !emptyOut) return NTLSTATUS_INVALID_PARAMETER; uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return NTLSTATUS_NO_MEMORY; uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, dirCluster, &cursor, &lba, §ors)) { MmFreePool(clusterBuf); return NTLSTATUS_INVALID_PARAMETER; } do { KSTATUS s = FatReadSectors(v, lba, sectors, clusterBuf); if (!KSUCCESS(s)) { MmFreePool(clusterBuf); return s; } uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; for (uint32_t i = 0; i < entries; i++) { uint8_t first = (uint8_t)dir[i].Name[0]; if (first == 0x00) { MmFreePool(clusterBuf); *emptyOut = 1; return NTLSTATUS_SUCCESS; } if (first == 0xE5 || dir[i].Attr == ATTR_LFN) continue; if ((dir[i].Attr & ATTR_VOLUME_ID) && !(dir[i].Attr & ATTR_DIRECTORY)) continue; if (FatIsDotEntry(&dir[i])) continue; MmFreePool(clusterBuf); *emptyOut = 0; return NTLSTATUS_SUCCESS; } } while (FatDirectoryNextSegment(v, dirCluster, &cursor, &lba, §ors)); MmFreePool(clusterBuf); *emptyOut = 1; return NTLSTATUS_SUCCESS; } // Deletes the directory-entry run for one child name from a parent directory. KSTATUS FatDeleteDirentRunV(FAT_VOL* v, uint32_t parentCluster, SYS_STRING_VIEW name) { if (!v || !FatNameViewIsValid(name)) return NTLSTATUS_INVALID_PARAMETER; uint8_t* clusterBuf = (uint8_t*)MmAllocatePool(v->BytesPerCluster); if (!clusterBuf) return NTLSTATUS_NO_MEMORY; char lfn[FAT_MAX_LFN]; bool hasLfn = false; uint32_t lfnStart = 0; memset(lfn, 0, sizeof(lfn)); char wantName83[8], wantExt83[3]; bool allowShortMatch = FatNameIsStructurallyShort83V(name); Make83NameV(name, wantName83, wantExt83); uint32_t cursor; uint64_t lba; uint32_t sectors; if (!FatDirectoryFirstSegment(v, parentCluster, &cursor, &lba, §ors)) { MmFreePool(clusterBuf); return NTLSTATUS_NOT_FOUND; } do { KSTATUS s = FatReadSectors(v, lba, sectors, clusterBuf); if (!KSUCCESS(s)) { MmFreePool(clusterBuf); return s; } uint32_t entries = (sectors * v->BytesPerSector) / sizeof(DIRENT); DIRENT* dir = (DIRENT*)clusterBuf; hasLfn = false; memset(lfn, 0, sizeof(lfn)); for (uint32_t i = 0; i < entries; i++) { uint8_t first = (uint8_t)dir[i].Name[0]; if (first == 0x00) { MmFreePool(clusterBuf); return NTLSTATUS_NOT_FOUND; } if (first == 0xE5) { hasLfn = false; memset(lfn, 0, sizeof(lfn)); continue; } if (dir[i].Attr == ATTR_LFN) { if (!hasLfn) lfnStart = i; FatLfnAppend(lfn, (LFN_ENTRY*)&dir[i]); hasLfn = true; continue; } bool match = false; if (hasLfn && lfn[0]) match = LfnMatchV(lfn, name); if (!match && allowShortMatch) match = MatchName83V(dir[i].Name, dir[i].Ext, name); if (!match && allowShortMatch) match = MatchRaw83(dir[i].Name, dir[i].Ext, wantName83, wantExt83); if (match) { uint32_t start = hasLfn ? lfnStart : i; for (uint32_t j = start; j <= i; j++) dir[j].Name[0] = (char)0xE5; s = FatWriteSectors(v, lba, sectors, clusterBuf); MmFreePool(clusterBuf); if (KSUCCESS(s)) FatPathCacheInvalidate(v); return s; } hasLfn = false; memset(lfn, 0, sizeof(lfn)); } } while (FatDirectoryNextSegment(v, parentCluster, &cursor, &lba, §ors)); MmFreePool(clusterBuf); return NTLSTATUS_NOT_FOUND; }