/* * Fat32.cpp * FAT32 filesystem driver * Copyright (c) 2026 Daniel Hammer */ #include "Fat32.hpp" #include "FsProbe.hpp" #include #include #include #include using namespace Kt; namespace Fs::Fat32 { // ========================================================================= // Constants // ========================================================================= static constexpr int MaxInstances = 8; static constexpr int MaxFilesPerInstance = 16; static constexpr int MaxDirEntries = 128; static constexpr int MaxNameLen = 256; // Directory entry attributes static constexpr uint8_t ATTR_READ_ONLY = 0x01; static constexpr uint8_t ATTR_HIDDEN = 0x02; static constexpr uint8_t ATTR_SYSTEM = 0x04; static constexpr uint8_t ATTR_VOLUME_ID = 0x08; static constexpr uint8_t ATTR_DIRECTORY = 0x10; static constexpr uint8_t ATTR_ARCHIVE = 0x20; static constexpr uint8_t ATTR_LFN = 0x0F; // Cluster chain sentinels static constexpr uint32_t CLUSTER_FREE = 0x00000000; static constexpr uint32_t CLUSTER_BAD = 0x0FFFFFF7; static constexpr uint32_t CLUSTER_END_MIN = 0x0FFFFFF8; // ========================================================================= // Types // ========================================================================= struct Fat32File { bool inUse; uint32_t firstCluster; uint32_t fileSize; bool isDirectory; // Location of the 32-byte SFN directory entry on disk uint64_t sfnPartSector; // partition-relative sector uint32_t sfnOffInSector; // byte offset within that sector // Cached position for sequential access (avoids O(n²) chain walks) uint32_t cachedClusterIdx; // cluster index in chain uint32_t cachedCluster; // cluster number at that index }; struct Fat32Instance { bool active; int blockDevIndex; uint64_t partStartLba; // BPB fields uint16_t bytesPerSector; uint8_t sectorsPerCluster; uint16_t reservedSectors; uint8_t numFats; uint32_t fatSize32; uint32_t rootCluster; uint32_t totalSectors; char volumeLabel[12]; // Computed uint32_t clusterSize; // bytesPerSector * sectorsPerCluster uint32_t dataStartSector; // partition-relative sector of first data cluster uint32_t clusterCount; // Cluster read buffer (page-allocated) uint8_t* clusterBuf; int clusterBufPages; // In-memory FAT cache (page-allocated) uint32_t* fatCache; int fatCachePages; uint32_t fatCacheEntries; // number of valid 4-byte entries // Open file handles Fat32File files[MaxFilesPerInstance]; // ReadDir name cache char dirNames[MaxDirEntries][MaxNameLen]; int dirNameCount; }; struct ParsedEntry { char name[MaxNameLen]; uint32_t firstCluster; uint32_t fileSize; uint8_t attributes; // Location of the SFN entry on disk (for write support) uint64_t sfnPartSector; uint32_t sfnOffInSector; }; // ========================================================================= // Instance table // ========================================================================= static Fat32Instance g_instances[MaxInstances] = {}; static int g_instanceCount = 0; // ========================================================================= // Low-level helpers // ========================================================================= static bool ReadPartSectors(const Fat32Instance& inst, uint64_t partSector, uint32_t count, void* buf) { auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex); if (!dev) return false; return dev->ReadSectors(dev->Ctx, inst.partStartLba + partSector, count, buf); } static uint64_t ClusterToPartSector(const Fat32Instance& inst, uint32_t cluster) { return (uint64_t)inst.dataStartSector + (uint64_t)(cluster - 2) * inst.sectorsPerCluster; } static bool ReadCluster(Fat32Instance& inst, uint32_t cluster) { if (cluster < 2) return false; return ReadPartSectors(inst, ClusterToPartSector(inst, cluster), inst.sectorsPerCluster, inst.clusterBuf); } static uint32_t GetNextCluster(const Fat32Instance& inst, uint32_t cluster) { // Use in-memory FAT cache if available (avoids disk I/O per lookup) if (inst.fatCache && cluster < inst.fatCacheEntries) { return inst.fatCache[cluster] & 0x0FFFFFFF; } // Fallback: read from disk uint32_t fatOffset = cluster * 4; uint32_t fatSector = fatOffset / inst.bytesPerSector; uint32_t entryOffset = fatOffset % inst.bytesPerSector; uint8_t sectorBuf[512]; if (!ReadPartSectors(inst, inst.reservedSectors + fatSector, 1, sectorBuf)) { return CLUSTER_END_MIN; // treat read error as end of chain } uint32_t entry; memcpy(&entry, sectorBuf + entryOffset, 4); return entry & 0x0FFFFFFF; } static bool IsEndOfChain(uint32_t cluster) { return cluster >= CLUSTER_END_MIN || cluster == CLUSTER_BAD || cluster < 2; } // ========================================================================= // Write helpers // ========================================================================= static bool WritePartSectors(const Fat32Instance& inst, uint64_t partSector, uint32_t count, const void* buf) { auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex); if (!dev) return false; return dev->WriteSectors(dev->Ctx, inst.partStartLba + partSector, count, buf); } static bool WriteClusterData(Fat32Instance& inst, uint32_t cluster, const void* data) { if (cluster < 2) return false; return WritePartSectors(inst, ClusterToPartSector(inst, cluster), inst.sectorsPerCluster, data); } static bool WriteFatEntry(Fat32Instance& inst, uint32_t cluster, uint32_t value) { uint32_t fatOffset = cluster * 4; uint32_t fatSector = fatOffset / inst.bytesPerSector; uint32_t entryOffset = fatOffset % inst.bytesPerSector; uint8_t sectorBuf[512]; for (int f = 0; f < inst.numFats; f++) { uint64_t fatStart = inst.reservedSectors + (uint64_t)f * inst.fatSize32; if (!ReadPartSectors(inst, fatStart + fatSector, 1, sectorBuf)) return false; // Preserve upper 4 bits of the existing entry uint32_t existing; memcpy(&existing, sectorBuf + entryOffset, 4); uint32_t merged = (existing & 0xF0000000) | (value & 0x0FFFFFFF); memcpy(sectorBuf + entryOffset, &merged, 4); if (!WritePartSectors(inst, fatStart + fatSector, 1, sectorBuf)) return false; } // Keep in-memory FAT cache in sync if (inst.fatCache && cluster < inst.fatCacheEntries) { uint32_t existing = inst.fatCache[cluster]; inst.fatCache[cluster] = (existing & 0xF0000000) | (value & 0x0FFFFFFF); } return true; } static uint32_t AllocateCluster(Fat32Instance& inst) { // Fast path: scan the in-memory FAT cache if (inst.fatCache) { uint32_t limit = inst.clusterCount + 2; if (limit > inst.fatCacheEntries) limit = inst.fatCacheEntries; for (uint32_t cluster = 2; cluster < limit; cluster++) { if ((inst.fatCache[cluster] & 0x0FFFFFFF) == CLUSTER_FREE) { if (!WriteFatEntry(inst, cluster, 0x0FFFFFFF)) return 0; return cluster; } } return 0; } // Slow path: read FAT sectors from disk uint8_t sectorBuf[512]; uint32_t entriesPerSector = inst.bytesPerSector / 4; for (uint32_t s = 0; s < inst.fatSize32; s++) { if (!ReadPartSectors(inst, inst.reservedSectors + s, 1, sectorBuf)) continue; for (uint32_t e = 0; e < entriesPerSector; e++) { uint32_t cluster = s * entriesPerSector + e; if (cluster < 2) continue; if (cluster >= inst.clusterCount + 2) return 0; uint32_t entry; memcpy(&entry, sectorBuf + e * 4, 4); if ((entry & 0x0FFFFFFF) == CLUSTER_FREE) { if (!WriteFatEntry(inst, cluster, 0x0FFFFFFF)) return 0; return cluster; } } } return 0; } // Update the file size field in a file's SFN directory entry on disk static bool UpdateDirEntrySize(Fat32Instance& inst, const Fat32File& file) { uint8_t sectorBuf[512]; if (!ReadPartSectors(inst, file.sfnPartSector, 1, sectorBuf)) return false; memcpy(sectorBuf + file.sfnOffInSector + 28, &file.fileSize, 4); return WritePartSectors(inst, file.sfnPartSector, 1, sectorBuf); } // Update both first-cluster and file-size in a file's SFN directory entry static bool UpdateDirEntry(Fat32Instance& inst, const Fat32File& file) { uint8_t sectorBuf[512]; if (!ReadPartSectors(inst, file.sfnPartSector, 1, sectorBuf)) return false; uint8_t* e = sectorBuf + file.sfnOffInSector; uint16_t clHi = (uint16_t)(file.firstCluster >> 16); uint16_t clLo = (uint16_t)(file.firstCluster & 0xFFFF); memcpy(e + 20, &clHi, 2); memcpy(e + 26, &clLo, 2); memcpy(e + 28, &file.fileSize, 4); return WritePartSectors(inst, file.sfnPartSector, 1, sectorBuf); } // ========================================================================= // Short name generation and LFN helpers for Create // ========================================================================= static void GenerateShortName(const char* longName, char* shortName) { memset(shortName, ' ', 11); // Find last dot int lastDot = -1; int nameLen = 0; for (int i = 0; longName[i]; i++) { nameLen = i + 1; if (longName[i] == '.') lastDot = i; } // Base name (up to 8 chars, uppercase, skip invalid chars) int baseEnd = (lastDot >= 0) ? lastDot : nameLen; int j = 0; for (int i = 0; i < baseEnd && j < 8; i++) { char c = longName[i]; if (c == ' ' || c == '.') continue; if (c >= 'a' && c <= 'z') c -= 32; shortName[j++] = c; } // Extension (up to 3 chars, uppercase) if (lastDot >= 0) { j = 0; for (int i = lastDot + 1; longName[i] && j < 3; i++) { char c = longName[i]; if (c >= 'a' && c <= 'z') c -= 32; shortName[8 + j++] = c; } } } // Check if a short name already exists in a directory static bool ShortNameExists(int inst, uint32_t dirCluster, const char* shortName) { auto& self = g_instances[inst]; uint32_t cluster = dirCluster; while (!IsEndOfChain(cluster)) { if (!ReadCluster(self, cluster)) return false; int perCluster = (int)(self.clusterSize / 32); for (int i = 0; i < perCluster; i++) { uint8_t* e = self.clusterBuf + i * 32; if (e[0] == 0x00) return false; if (e[0] == 0xE5) continue; if (e[11] == ATTR_LFN) continue; if (memcmp(e, shortName, 11) == 0) return true; } cluster = GetNextCluster(self, cluster); } return false; } // Make the short name unique by appending ~N static void MakeShortNameUnique(int inst, uint32_t dirCluster, char* shortName) { if (!ShortNameExists(inst, dirCluster, shortName)) return; // Find base length (non-space chars in name portion) int baseLen = 8; while (baseLen > 0 && shortName[baseLen - 1] == ' ') baseLen--; for (int n = 1; n <= 999; n++) { // Build suffix like "~1", "~23" char suffix[8]; suffix[0] = '~'; int sLen = 1; int tmp = n; char digits[4]; int dLen = 0; while (tmp > 0) { digits[dLen++] = '0' + (tmp % 10); tmp /= 10; } for (int d = dLen - 1; d >= 0; d--) suffix[sLen++] = digits[d]; int maxBase = 8 - sLen; if (baseLen < maxBase) maxBase = baseLen; char trial[11]; memcpy(trial, shortName, 11); // Overwrite end of base with suffix for (int i = 0; i < sLen; i++) trial[maxBase + i] = suffix[i]; // Pad remainder with spaces for (int i = maxBase + sLen; i < 8; i++) trial[i] = ' '; if (!ShortNameExists(inst, dirCluster, trial)) { memcpy(shortName, trial, 11); return; } } } static uint8_t LfnChecksum(const char* shortName) { uint8_t sum = 0; for (int i = 0; i < 11; i++) { sum = ((sum & 1) ? 0x80 : 0) + (sum >> 1) + (uint8_t)shortName[i]; } return sum; } // Build LFN directory entries in disk order (highest seq first). // Returns the number of LFN entries. static int BuildLfnEntries(const char* longName, uint8_t checksum, uint8_t* outEntries) { // Convert to UTF-16 uint16_t utf16[MaxNameLen]; int len = 0; while (longName[len] && len < MaxNameLen - 1) { utf16[len] = (uint8_t)longName[len]; len++; } utf16[len] = 0; int totalChars = len + 1; // include null terminator int numEntries = (totalChars + 12) / 13; // Pad remaining with 0xFFFF for (int i = totalChars; i < numEntries * 13; i++) { utf16[i] = 0xFFFF; } // Build in disk order: entry N (0x40|N), then N-1, ..., 1 for (int e = 0; e < numEntries; e++) { int seqNum = numEntries - e; // disk order: highest first uint8_t* ent = outEntries + e * 32; memset(ent, 0, 32); ent[0] = (uint8_t)seqNum; if (e == 0) ent[0] |= 0x40; // first physical = last logical ent[11] = ATTR_LFN; ent[13] = checksum; int base = (seqNum - 1) * 13; // Chars 1-5 at offset 1 for (int i = 0; i < 5; i++) memcpy(ent + 1 + i * 2, &utf16[base + i], 2); // Chars 6-11 at offset 14 for (int i = 0; i < 6; i++) memcpy(ent + 14 + i * 2, &utf16[base + 5 + i], 2); // Chars 12-13 at offset 28 for (int i = 0; i < 2; i++) memcpy(ent + 28 + i * 2, &utf16[base + 11 + i], 2); } return numEntries; } // Forward declaration (defined below in case-insensitive comparison section) static bool StrEqualNoCase(const char* a, const char* b); // Check if a short name is needed (i.e. long name differs from 8.3) static bool NeedsLfn(const char* longName, const char* shortName) { // If name contains lowercase, spaces, or is longer than 8.3, need LFN char reconstructed[13]; int pos = 0; for (int i = 0; i < 8 && shortName[i] != ' '; i++) reconstructed[pos++] = shortName[i]; if (shortName[8] != ' ') { reconstructed[pos++] = '.'; for (int i = 8; i < 11 && shortName[i] != ' '; i++) reconstructed[pos++] = shortName[i]; } reconstructed[pos] = '\0'; // Compare case-insensitively if (!StrEqualNoCase(longName, reconstructed)) return true; // Check if long name has lowercase (short names are uppercase) for (int i = 0; longName[i]; i++) { if (longName[i] >= 'a' && longName[i] <= 'z') return true; } return false; } // ========================================================================= // Directory entry allocation for Create // ========================================================================= // Find `slotsNeeded` consecutive free (0x00 or 0xE5) 32-byte entries in a // directory. If there isn't room, extends the directory by allocating a new // cluster. Returns the partition-relative sector and byte offset of the // first free slot, plus the cluster it's in. struct DirSlotPos { uint64_t partSector; // partition-relative sector of the first slot uint32_t offsetInSec; // byte offset within that sector bool found; }; static DirSlotPos FindFreeDirSlots(int inst, uint32_t dirFirstCluster, int slotsNeeded) { auto& self = g_instances[inst]; uint32_t cluster = dirFirstCluster; uint32_t prevCluster = 0; while (!IsEndOfChain(cluster)) { if (!ReadCluster(self, cluster)) return {0, 0, false}; int perCluster = (int)(self.clusterSize / 32); int consecutive = 0; int startIdx = 0; for (int i = 0; i < perCluster; i++) { uint8_t* e = self.clusterBuf + i * 32; if (e[0] == 0x00 || e[0] == 0xE5) { if (consecutive == 0) startIdx = i; consecutive++; if (consecutive >= slotsNeeded) { // Found enough slots uint64_t clusterPartSec = ClusterToPartSector(self, cluster); uint32_t byteOff = startIdx * 32; uint64_t sector = clusterPartSec + (byteOff / self.bytesPerSector); uint32_t offInSec = byteOff % self.bytesPerSector; return {sector, offInSec, true}; } } else { consecutive = 0; } } prevCluster = cluster; cluster = GetNextCluster(self, cluster); } // No room — extend directory with a new cluster uint32_t newCluster = AllocateCluster(self); if (newCluster == 0) return {0, 0, false}; // Link previous last cluster to new one if (prevCluster != 0) { WriteFatEntry(self, prevCluster, newCluster); } // Zero the new cluster memset(self.clusterBuf, 0, self.clusterSize); WriteClusterData(self, newCluster, self.clusterBuf); uint64_t sec = ClusterToPartSector(self, newCluster); return {sec, 0, true}; } // Split a path into parent directory path and filename. // E.g. "foo/bar/baz.txt" → parentPath="foo/bar", name="baz.txt" // For "baz.txt" → parentPath="", name="baz.txt" static void SplitPath(const char* path, char* parentPath, int parentMax, char* fileName, int nameMax) { // Skip leading slashes while (*path == '/') path++; int len = 0; while (path[len]) len++; // Find last slash int lastSlash = -1; for (int i = len - 1; i >= 0; i--) { if (path[i] == '/') { lastSlash = i; break; } } if (lastSlash < 0) { parentPath[0] = '\0'; int j = 0; while (j < nameMax - 1 && path[j]) { fileName[j] = path[j]; j++; } fileName[j] = '\0'; } else { int j = 0; for (int i = 0; i < lastSlash && j < parentMax - 1; i++) { parentPath[j++] = path[i]; } parentPath[j] = '\0'; j = 0; for (int i = lastSlash + 1; i < len && j < nameMax - 1; i++) { fileName[j++] = path[i]; } fileName[j] = '\0'; } } // ========================================================================= // Short name parsing // ========================================================================= static void ParseShortName(const uint8_t* entry, char* out) { int pos = 0; uint8_t ntRes = entry[12]; // NTRes case flags // Base name (8 bytes, trim trailing spaces) for (int i = 0; i < 8 && entry[i] != ' '; i++) { char c = (char)entry[i]; if (ntRes & 0x08) { // lowercase name if (c >= 'A' && c <= 'Z') c += 32; } out[pos++] = c; } // Extension (3 bytes) if (entry[8] != ' ') { out[pos++] = '.'; for (int i = 8; i < 11 && entry[i] != ' '; i++) { char c = (char)entry[i]; if (ntRes & 0x10) { // lowercase extension if (c >= 'A' && c <= 'Z') c += 32; } out[pos++] = c; } } out[pos] = '\0'; } // ========================================================================= // LFN support // ========================================================================= static void ExtractLfnChars(const uint8_t* entry, uint16_t* out) { // Chars 1-5 at offset 1 for (int i = 0; i < 5; i++) memcpy(&out[i], entry + 1 + i * 2, 2); // Chars 6-11 at offset 14 for (int i = 0; i < 6; i++) memcpy(&out[5 + i], entry + 14 + i * 2, 2); // Chars 12-13 at offset 28 for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2); } static void Utf16ToAscii(const uint16_t* src, int maxLen, char* dst) { int j = 0; for (int i = 0; i < maxLen && src[i] != 0 && j < MaxNameLen - 1; i++) { dst[j++] = (src[i] < 128) ? (char)src[i] : '?'; } dst[j] = '\0'; } // ========================================================================= // Directory reading // ========================================================================= static int ReadDirectory(int inst, uint32_t dirCluster, ParsedEntry* entries, int maxEntries) { auto& self = g_instances[inst]; uint16_t lfnBuf[MaxNameLen]; bool hasLfn = false; int count = 0; uint32_t cluster = dirCluster; while (!IsEndOfChain(cluster) && count < maxEntries) { if (!ReadCluster(self, cluster)) break; int perCluster = (int)(self.clusterSize / 32); for (int i = 0; i < perCluster && count < maxEntries; i++) { uint8_t* e = self.clusterBuf + i * 32; if (e[0] == 0x00) return count; // end of directory if (e[0] == 0xE5) { hasLfn = false; continue; } // deleted uint8_t attr = e[11]; if (attr == ATTR_LFN) { uint8_t seq = e[0]; int seqNum = seq & 0x1F; if (seq & 0x40) { // Last logical (first physical) LFN fragment for (int k = 0; k < MaxNameLen; k++) lfnBuf[k] = 0; hasLfn = true; } if (hasLfn && seqNum >= 1 && seqNum <= 20) { uint16_t chars[13]; ExtractLfnChars(e, chars); int offset = (seqNum - 1) * 13; for (int k = 0; k < 13 && offset + k < MaxNameLen; k++) { lfnBuf[offset + k] = chars[k]; } } continue; } // Skip volume label entries if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; } ParsedEntry& pe = entries[count]; uint16_t clusterHi, clusterLo; memcpy(&clusterHi, e + 20, 2); memcpy(&clusterLo, e + 26, 2); pe.firstCluster = ((uint32_t)clusterHi << 16) | (uint32_t)clusterLo; memcpy(&pe.fileSize, e + 28, 4); pe.attributes = attr; if (hasLfn) { Utf16ToAscii(lfnBuf, MaxNameLen, pe.name); } else { ParseShortName(e, pe.name); } hasLfn = false; // Skip '.' and '..' entries if (pe.name[0] == '.' && (pe.name[1] == '\0' || (pe.name[1] == '.' && pe.name[2] == '\0'))) { continue; } count++; } cluster = GetNextCluster(self, cluster); } return count; } // ========================================================================= // Case-insensitive string comparison // ========================================================================= static char ToLower(char c) { return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; } static bool StrEqualNoCase(const char* a, const char* b) { while (*a && *b) { if (ToLower(*a) != ToLower(*b)) return false; a++; b++; } return *a == *b; } // ========================================================================= // Path traversal // ========================================================================= // Find a single entry by name in a directory. // Returns true and fills 'out' on success. static bool FindInDirectory(int inst, uint32_t dirCluster, const char* name, ParsedEntry* out) { uint32_t cluster = dirCluster; auto& self = g_instances[inst]; uint16_t lfnBuf[MaxNameLen]; bool hasLfn = false; while (!IsEndOfChain(cluster)) { if (!ReadCluster(self, cluster)) return false; int perCluster = (int)(self.clusterSize / 32); for (int i = 0; i < perCluster; i++) { uint8_t* e = self.clusterBuf + i * 32; if (e[0] == 0x00) return false; if (e[0] == 0xE5) { hasLfn = false; continue; } uint8_t attr = e[11]; if (attr == ATTR_LFN) { uint8_t seq = e[0]; int seqNum = seq & 0x1F; if (seq & 0x40) { for (int k = 0; k < MaxNameLen; k++) lfnBuf[k] = 0; hasLfn = true; } if (hasLfn && seqNum >= 1 && seqNum <= 20) { uint16_t chars[13]; ExtractLfnChars(e, chars); int offset = (seqNum - 1) * 13; for (int k = 0; k < 13 && offset + k < MaxNameLen; k++) { lfnBuf[offset + k] = chars[k]; } } continue; } if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; } char entryName[MaxNameLen]; if (hasLfn) { Utf16ToAscii(lfnBuf, MaxNameLen, entryName); } else { ParseShortName(e, entryName); } hasLfn = false; if (StrEqualNoCase(entryName, name)) { uint16_t clHi, clLo; memcpy(&clHi, e + 20, 2); memcpy(&clLo, e + 26, 2); out->firstCluster = ((uint32_t)clHi << 16) | (uint32_t)clLo; memcpy(&out->fileSize, e + 28, 4); out->attributes = attr; int j = 0; while (entryName[j] && j < MaxNameLen - 1) { out->name[j] = entryName[j]; j++; } out->name[j] = '\0'; // Record SFN entry disk location uint32_t byteOff = i * 32; uint64_t clusterPartSec = ClusterToPartSector(self, cluster); out->sfnPartSector = clusterPartSec + (byteOff / self.bytesPerSector); out->sfnOffInSector = byteOff % self.bytesPerSector; return true; } } cluster = GetNextCluster(self, cluster); } return false; } // Traverse a full path from root. Returns true and fills 'out'. // For "/" or "" returns the root directory pseudo-entry. static bool TraversePath(int inst, const char* path, ParsedEntry* out) { auto& self = g_instances[inst]; // Skip leading '/' while (*path == '/') path++; // Empty path = root directory if (*path == '\0') { out->firstCluster = self.rootCluster; out->fileSize = 0; out->attributes = ATTR_DIRECTORY; out->name[0] = '/'; out->name[1] = '\0'; return true; } uint32_t currentCluster = self.rootCluster; while (*path) { // Extract next path component char component[MaxNameLen]; int len = 0; while (*path && *path != '/' && len < MaxNameLen - 1) { component[len++] = *path++; } component[len] = '\0'; // Skip trailing slashes while (*path == '/') path++; ParsedEntry found; if (!FindInDirectory(inst, currentCluster, component, &found)) { return false; } if (*path == '\0') { // This is the final component *out = found; return true; } // Must be a directory to continue traversal if (!(found.attributes & ATTR_DIRECTORY)) return false; currentCluster = found.firstCluster; } return false; } // ========================================================================= // FsDriver implementation functions // ========================================================================= static int OpenImpl(int inst, const char* path) { if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; ParsedEntry entry; if (!TraversePath(inst, path, &entry)) return -1; // Find a free file handle auto& self = g_instances[inst]; for (int i = 0; i < MaxFilesPerInstance; i++) { if (!self.files[i].inUse) { self.files[i].inUse = true; self.files[i].firstCluster = entry.firstCluster; self.files[i].fileSize = entry.fileSize; self.files[i].isDirectory = (entry.attributes & ATTR_DIRECTORY) != 0; self.files[i].sfnPartSector = entry.sfnPartSector; self.files[i].sfnOffInSector = entry.sfnOffInSector; self.files[i].cachedClusterIdx = 0; self.files[i].cachedCluster = entry.firstCluster; return i; } } return -1; // no free handle } static int ReadImpl(int inst, int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { if (inst < 0 || inst >= g_instanceCount) return -1; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; auto& file = self.files[handle]; // Directories don't have a meaningful fileSize for reading if (file.isDirectory) return -1; if (offset >= file.fileSize) return 0; if (offset + size > file.fileSize) size = file.fileSize - offset; if (size == 0) return 0; uint32_t clusterSize = self.clusterSize; uint32_t startClusterIdx = (uint32_t)(offset / clusterSize); uint32_t clusterOff = (uint32_t)(offset % clusterSize); // Walk cluster chain to starting cluster (use cache if possible) uint32_t cluster; uint32_t startFrom = 0; if (file.cachedCluster >= 2 && file.cachedClusterIdx <= startClusterIdx) { cluster = file.cachedCluster; startFrom = file.cachedClusterIdx; } else { cluster = file.firstCluster; } for (uint32_t i = startFrom; i < startClusterIdx; i++) { cluster = GetNextCluster(self, cluster); if (IsEndOfChain(cluster)) return 0; } // Read data cluster by cluster uint64_t bytesRead = 0; uint32_t lastCluster = cluster; while (bytesRead < size && !IsEndOfChain(cluster)) { if (!ReadCluster(self, cluster)) break; uint32_t available = clusterSize - clusterOff; uint64_t toRead = size - bytesRead; if (toRead > available) toRead = available; memcpy(buffer + bytesRead, self.clusterBuf + clusterOff, toRead); bytesRead += toRead; clusterOff = 0; // subsequent clusters start from offset 0 lastCluster = cluster; cluster = GetNextCluster(self, cluster); } // Update cache if (bytesRead > 0 && lastCluster >= 2) { uint32_t endClusterIdx = (uint32_t)((offset + bytesRead - 1) / clusterSize); file.cachedClusterIdx = endClusterIdx; file.cachedCluster = lastCluster; } return (int)bytesRead; } static uint64_t GetSizeImpl(int inst, int handle) { if (inst < 0 || inst >= g_instanceCount) return 0; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return 0; return self.files[handle].fileSize; } static void CloseImpl(int inst, int handle) { if (inst < 0 || inst >= g_instanceCount) return; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance) return; self.files[handle].inUse = false; } static int ReadDirImpl(int inst, const char* path, const char** outNames, int maxEntries) { if (inst < 0 || inst >= g_instanceCount) return -1; auto& self = g_instances[inst]; ParsedEntry dirEntry; if (!TraversePath(inst, path, &dirEntry)) return -1; if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1; ParsedEntry entries[MaxDirEntries]; int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries; int count = ReadDirectory(inst, dirEntry.firstCluster, entries, limit); // Copy names into persistent cache self.dirNameCount = count; for (int i = 0; i < count; i++) { int j = 0; while (entries[i].name[j] && j < MaxNameLen - 2) { self.dirNames[i][j] = entries[i].name[j]; j++; } // Append trailing '/' for directories so userspace can distinguish them if ((entries[i].attributes & ATTR_DIRECTORY) && j < MaxNameLen - 1) { self.dirNames[i][j++] = '/'; } self.dirNames[i][j] = '\0'; outNames[i] = self.dirNames[i]; } return count; } static int WriteImpl(int inst, int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { if (inst < 0 || inst >= g_instanceCount) return -1; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; auto& file = self.files[handle]; if (file.isDirectory) return -1; if (size == 0) return 0; uint32_t clusterSize = self.clusterSize; // If file has no clusters yet, allocate the first one if (file.firstCluster < 2) { uint32_t cl = AllocateCluster(self); if (cl == 0) return -1; file.firstCluster = cl; // Zero the new cluster memset(self.clusterBuf, 0, clusterSize); WriteClusterData(self, cl, self.clusterBuf); UpdateDirEntry(self, file); } uint64_t bytesWritten = 0; // Walk cluster chain to the starting cluster for this offset uint32_t clusterIdx = (uint32_t)(offset / clusterSize); uint32_t clusterOff = (uint32_t)(offset % clusterSize); // Use cached position if we can skip ahead uint32_t cluster; uint32_t prevCluster = 0; uint32_t startIdx = 0; if (file.cachedCluster >= 2 && file.cachedClusterIdx <= clusterIdx) { cluster = file.cachedCluster; startIdx = file.cachedClusterIdx; } else { cluster = file.firstCluster; } for (uint32_t i = startIdx; i < clusterIdx; i++) { prevCluster = cluster; uint32_t next = GetNextCluster(self, cluster); if (IsEndOfChain(next)) { // Need to extend the chain next = AllocateCluster(self); if (next == 0) goto done; WriteFatEntry(self, cluster, next); memset(self.clusterBuf, 0, clusterSize); WriteClusterData(self, next, self.clusterBuf); } cluster = next; } // Write data cluster by cluster while (bytesWritten < size) { if (IsEndOfChain(cluster) || cluster < 2) { // Allocate a new cluster and link it uint32_t newCl = AllocateCluster(self); if (newCl == 0) goto done; if (prevCluster != 0) { WriteFatEntry(self, prevCluster, newCl); } memset(self.clusterBuf, 0, clusterSize); WriteClusterData(self, newCl, self.clusterBuf); cluster = newCl; } // Read existing cluster data (for partial writes) if (!ReadCluster(self, cluster)) goto done; uint32_t available = clusterSize - clusterOff; uint64_t toWrite = size - bytesWritten; if (toWrite > available) toWrite = available; memcpy(self.clusterBuf + clusterOff, buffer + bytesWritten, toWrite); if (!WriteClusterData(self, cluster, self.clusterBuf)) goto done; bytesWritten += toWrite; clusterOff = 0; prevCluster = cluster; cluster = GetNextCluster(self, cluster); } done: // Update cached position for sequential access if (bytesWritten > 0 && prevCluster >= 2) { uint32_t endClusterIdx = (uint32_t)((offset + bytesWritten - 1) / clusterSize); file.cachedClusterIdx = endClusterIdx; file.cachedCluster = prevCluster; } // Update file size if we wrote past the end uint64_t endPos = offset + bytesWritten; if (endPos > file.fileSize) { file.fileSize = (uint32_t)endPos; UpdateDirEntrySize(self, file); } return (int)bytesWritten; } static int CreateImpl(int inst, const char* path) { if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; auto& self = g_instances[inst]; // Split path into parent directory and filename char parentPath[MaxNameLen]; char fileName[MaxNameLen]; SplitPath(path, parentPath, MaxNameLen, fileName, MaxNameLen); if (fileName[0] == '\0') return -1; // Traverse to parent directory ParsedEntry parentEntry; if (!TraversePath(inst, parentPath, &parentEntry)) return -1; if (!(parentEntry.attributes & ATTR_DIRECTORY)) return -1; uint32_t parentCluster = parentEntry.firstCluster; // If file already exists, truncate it (free its cluster chain, reset size) ParsedEntry existing; if (FindInDirectory(inst, parentCluster, fileName, &existing)) { if (existing.attributes & ATTR_DIRECTORY) return -1; // can't truncate a dir // Free the cluster chain uint32_t cl = existing.firstCluster; while (!IsEndOfChain(cl) && cl >= 2) { uint32_t next = GetNextCluster(self, cl); WriteFatEntry(self, cl, CLUSTER_FREE); cl = next; } // Update directory entry: zero size, zero first cluster uint8_t sectorBuf[512]; if (!ReadPartSectors(self, existing.sfnPartSector, 1, sectorBuf)) return -1; uint8_t* e = sectorBuf + existing.sfnOffInSector; uint32_t zero32 = 0; uint16_t zero16 = 0; memcpy(e + 20, &zero16, 2); // cluster high memcpy(e + 26, &zero16, 2); // cluster low memcpy(e + 28, &zero32, 4); // file size if (!WritePartSectors(self, existing.sfnPartSector, 1, sectorBuf)) return -1; // Open a handle to it for (int i = 0; i < MaxFilesPerInstance; i++) { if (!self.files[i].inUse) { self.files[i].inUse = true; self.files[i].firstCluster = 0; self.files[i].fileSize = 0; self.files[i].isDirectory = false; self.files[i].sfnPartSector = existing.sfnPartSector; self.files[i].sfnOffInSector = existing.sfnOffInSector; self.files[i].cachedClusterIdx = 0; self.files[i].cachedCluster = 0; return i; } } return -1; } // Generate 8.3 short name char shortName[11]; GenerateShortName(fileName, shortName); MakeShortNameUnique(inst, parentCluster, shortName); // Build LFN entries if needed uint8_t lfnEntries[20 * 32]; // max 20 LFN entries int lfnCount = 0; bool needsLfn = NeedsLfn(fileName, shortName); if (needsLfn) { uint8_t checksum = LfnChecksum(shortName); lfnCount = BuildLfnEntries(fileName, checksum, lfnEntries); } int totalSlots = lfnCount + 1; // LFN entries + SFN entry // Find free directory slots DirSlotPos pos = FindFreeDirSlots(inst, parentCluster, totalSlots); if (!pos.found) return -1; // Write the entries into the directory sector by sector // Build all entries (LFN + SFN) contiguously uint8_t allEntries[21 * 32]; // max 21 entries total if (lfnCount > 0) { memcpy(allEntries, lfnEntries, lfnCount * 32); } // Build the SFN entry uint8_t* sfn = allEntries + lfnCount * 32; memset(sfn, 0, 32); memcpy(sfn, shortName, 11); sfn[11] = ATTR_ARCHIVE; // firstCluster = 0, fileSize = 0 (file starts empty) // Write entries to disk, handling sector boundaries uint64_t curSector = pos.partSector; uint32_t curOff = pos.offsetInSec; uint8_t sectorBuf[512]; if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; int bytesTotal = totalSlots * 32; int written = 0; while (written < bytesTotal) { int space = (int)self.bytesPerSector - (int)curOff; int chunk = bytesTotal - written; if (chunk > space) chunk = space; memcpy(sectorBuf + curOff, allEntries + written, chunk); written += chunk; if (written < bytesTotal || chunk == space) { // Need to flush this sector and move to next if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; if (written < bytesTotal) { curSector++; curOff = 0; if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; } } else { // Last write didn't fill the sector if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; } } // Compute the SFN entry's disk location uint64_t sfnSector = pos.partSector + (pos.offsetInSec + lfnCount * 32) / self.bytesPerSector; uint32_t sfnOff = (pos.offsetInSec + lfnCount * 32) % self.bytesPerSector; // Open a handle for (int i = 0; i < MaxFilesPerInstance; i++) { if (!self.files[i].inUse) { self.files[i].inUse = true; self.files[i].firstCluster = 0; self.files[i].fileSize = 0; self.files[i].isDirectory = false; self.files[i].sfnPartSector = sfnSector; self.files[i].sfnOffInSector = sfnOff; self.files[i].cachedClusterIdx = 0; self.files[i].cachedCluster = 0; return i; } } return -1; } static int DeleteImpl(int inst, const char* path) { if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; auto& self = g_instances[inst]; // Split path into parent directory and filename char parentPath[MaxNameLen]; char fileName[MaxNameLen]; SplitPath(path, parentPath, MaxNameLen, fileName, MaxNameLen); if (fileName[0] == '\0') return -1; // Traverse to parent directory ParsedEntry parentEntry; if (!TraversePath(inst, parentPath, &parentEntry)) return -1; if (!(parentEntry.attributes & ATTR_DIRECTORY)) return -1; uint32_t parentCluster = parentEntry.firstCluster; // Find the entry ParsedEntry existing; if (!FindInDirectory(inst, parentCluster, fileName, &existing)) return -1; if (existing.attributes & ATTR_DIRECTORY) { // Only allow deleting empty directories ParsedEntry children[1]; int childCount = ReadDirectory(inst, existing.firstCluster, children, 1); if (childCount > 0) return -1; } // Free the cluster chain uint32_t cl = existing.firstCluster; while (!IsEndOfChain(cl) && cl >= 2) { uint32_t next = GetNextCluster(self, cl); WriteFatEntry(self, cl, CLUSTER_FREE); cl = next; } // Mark directory entries as deleted (0xE5) // Walk the directory again to find and mark LFN + SFN entries uint32_t cluster = parentCluster; bool inLfnRun = false; while (!IsEndOfChain(cluster)) { if (!ReadCluster(self, cluster)) return -1; int perCluster = (int)(self.clusterSize / 32); bool modified = false; for (int i = 0; i < perCluster; i++) { uint8_t* e = self.clusterBuf + i * 32; if (e[0] == 0x00) goto write_back; if (e[0] == 0xE5) { inLfnRun = false; continue; } uint8_t attr = e[11]; if (attr == ATTR_LFN) { uint8_t seq = e[0]; if (seq & 0x40) { // Start of a new LFN run — check if it belongs to our file // by peeking ahead to the SFN entry inLfnRun = false; // will set to true if we find the match // Count how many LFN entries in this run int lfnCount = seq & 0x1F; // Check if the SFN entry after the LFN sequence matches int sfnIdx = i + lfnCount; // The SFN might be in this cluster or a later one — only // handle the simple same-cluster case here. if (sfnIdx < perCluster) { uint8_t* sfnE = self.clusterBuf + sfnIdx * 32; uint64_t clusterPartSec = ClusterToPartSector(self, cluster); uint32_t byteOff = sfnIdx * 32; uint64_t sfnSec = clusterPartSec + (byteOff / self.bytesPerSector); uint32_t sfnOff = byteOff % self.bytesPerSector; if (sfnSec == existing.sfnPartSector && sfnOff == existing.sfnOffInSector) { inLfnRun = true; e[0] = 0xE5; modified = true; } } } else if (inLfnRun) { e[0] = 0xE5; modified = true; } continue; } if (inLfnRun || (!inLfnRun && attr != ATTR_LFN)) { // Check if this is the SFN entry we're looking for uint64_t clusterPartSec = ClusterToPartSector(self, cluster); uint32_t byteOff = i * 32; uint64_t entrySec = clusterPartSec + (byteOff / self.bytesPerSector); uint32_t entryOff = byteOff % self.bytesPerSector; if (entrySec == existing.sfnPartSector && entryOff == existing.sfnOffInSector) { e[0] = 0xE5; modified = true; inLfnRun = false; goto write_back; } } inLfnRun = false; } write_back: if (modified) { WriteClusterData(self, cluster, self.clusterBuf); } // Check if we already marked the SFN entry // (the goto write_back above handles early exit) { // Re-check if the SFN was in this cluster uint64_t clusterPartSec = ClusterToPartSector(self, cluster); uint64_t clusterEndSec = clusterPartSec + self.sectorsPerCluster; if (existing.sfnPartSector >= clusterPartSec && existing.sfnPartSector < clusterEndSec) { return 0; // done } } cluster = GetNextCluster(self, cluster); } return 0; } // ========================================================================= // Mkdir — create a directory // ========================================================================= static int MkdirImpl(int inst, const char* path) { if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; auto& self = g_instances[inst]; // Split path into parent directory and new dir name char parentPath[MaxNameLen]; char dirName[MaxNameLen]; SplitPath(path, parentPath, MaxNameLen, dirName, MaxNameLen); if (dirName[0] == '\0') return -1; // Traverse to parent directory ParsedEntry parentEntry; if (!TraversePath(inst, parentPath, &parentEntry)) return -1; if (!(parentEntry.attributes & ATTR_DIRECTORY)) return -1; uint32_t parentCluster = parentEntry.firstCluster; // If directory already exists, return success ParsedEntry existing; if (FindInDirectory(inst, parentCluster, dirName, &existing)) { if (existing.attributes & ATTR_DIRECTORY) return 0; return -1; // exists as a file } // Allocate a cluster for the new directory uint32_t newCluster = AllocateCluster(self); if (newCluster == 0) return -1; // Initialize the new directory cluster with . and .. entries memset(self.clusterBuf, 0, self.clusterSize); // "." entry — points to itself uint8_t* dot = self.clusterBuf; memset(dot, ' ', 11); dot[0] = '.'; dot[11] = ATTR_DIRECTORY; uint16_t clHi = (uint16_t)(newCluster >> 16); uint16_t clLo = (uint16_t)(newCluster & 0xFFFF); memcpy(dot + 20, &clHi, 2); memcpy(dot + 26, &clLo, 2); // ".." entry — points to parent uint8_t* dotdot = self.clusterBuf + 32; memset(dotdot, ' ', 11); dotdot[0] = '.'; dotdot[1] = '.'; dotdot[11] = ATTR_DIRECTORY; uint32_t parentCl = (parentCluster == self.rootCluster) ? 0 : parentCluster; clHi = (uint16_t)(parentCl >> 16); clLo = (uint16_t)(parentCl & 0xFFFF); memcpy(dotdot + 20, &clHi, 2); memcpy(dotdot + 26, &clLo, 2); if (!WriteClusterData(self, newCluster, self.clusterBuf)) return -1; // Generate 8.3 short name for the directory char shortName[11]; GenerateShortName(dirName, shortName); MakeShortNameUnique(inst, parentCluster, shortName); // Build LFN entries if needed uint8_t lfnEntries[20 * 32]; int lfnCount = 0; bool needsLfn = NeedsLfn(dirName, shortName); if (needsLfn) { uint8_t checksum = LfnChecksum(shortName); lfnCount = BuildLfnEntries(dirName, checksum, lfnEntries); } int totalSlots = lfnCount + 1; // Find free directory slots in parent DirSlotPos pos = FindFreeDirSlots(inst, parentCluster, totalSlots); if (!pos.found) return -1; // Build all entries (LFN + SFN) uint8_t allEntries[21 * 32]; if (lfnCount > 0) { memcpy(allEntries, lfnEntries, lfnCount * 32); } // Build the SFN entry with ATTR_DIRECTORY uint8_t* sfn = allEntries + lfnCount * 32; memset(sfn, 0, 32); memcpy(sfn, shortName, 11); sfn[11] = ATTR_DIRECTORY; clHi = (uint16_t)(newCluster >> 16); clLo = (uint16_t)(newCluster & 0xFFFF); memcpy(sfn + 20, &clHi, 2); memcpy(sfn + 26, &clLo, 2); // Directory size field is 0 per FAT spec // Write entries to disk uint64_t curSector = pos.partSector; uint32_t curOff = pos.offsetInSec; uint8_t sectorBuf[512]; if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; int bytesTotal = totalSlots * 32; int written = 0; while (written < bytesTotal) { int space = (int)self.bytesPerSector - (int)curOff; int chunk = bytesTotal - written; if (chunk > space) chunk = space; memcpy(sectorBuf + curOff, allEntries + written, chunk); written += chunk; if (written < bytesTotal || chunk == space) { if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; if (written < bytesTotal) { curSector++; curOff = 0; if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; } } else { if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; } } return 0; } // ========================================================================= // Template thunks — generate unique function pointers per instance // ========================================================================= template struct Thunks { static int Open(const char* p) { return OpenImpl(N, p); } static int Read(int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(N, h, b, o, s); } static uint64_t GetSize(int h) { return GetSizeImpl(N, h); } static void Close(int h) { CloseImpl(N, h); } static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); } static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); } static int Create(const char* p) { return CreateImpl(N, p); } static int Delete(const char* p) { return DeleteImpl(N, p); } static int Mkdir(const char* p) { return MkdirImpl(N, p); } }; template static Vfs::FsDriver MakeDriver() { return { Thunks::Open, Thunks::Read, Thunks::GetSize, Thunks::Close, Thunks::ReadDir, Thunks::Write, Thunks::Create, Thunks::Delete, Thunks::Mkdir, }; } static Vfs::FsDriver g_drivers[] = { MakeDriver<0>(), MakeDriver<1>(), MakeDriver<2>(), MakeDriver<3>(), MakeDriver<4>(), MakeDriver<5>(), MakeDriver<6>(), MakeDriver<7>(), }; // ========================================================================= // BPB validation and mount // ========================================================================= Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) { if (g_instanceCount >= MaxInstances) return nullptr; auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex); if (!dev) return nullptr; // Read the first sector of the partition (BPB / VBR) uint8_t bpb[512]; if (!dev->ReadSectors(dev->Ctx, startLba, 1, bpb)) return nullptr; // Validate boot signature if (bpb[510] != 0x55 || bpb[511] != 0xAA) return nullptr; // Parse BPB fields uint16_t bytesPerSector; memcpy(&bytesPerSector, bpb + 11, 2); if (bytesPerSector != 512 && bytesPerSector != 1024 && bytesPerSector != 2048 && bytesPerSector != 4096) return nullptr; uint8_t sectorsPerCluster = bpb[13]; if (sectorsPerCluster == 0 || (sectorsPerCluster & (sectorsPerCluster - 1)) != 0) return nullptr; // must be power of 2 uint16_t reservedSectors; memcpy(&reservedSectors, bpb + 14, 2); if (reservedSectors == 0) return nullptr; uint8_t numFats = bpb[16]; if (numFats == 0) return nullptr; // FAT32-specific checks uint16_t rootEntryCount; memcpy(&rootEntryCount, bpb + 17, 2); if (rootEntryCount != 0) return nullptr; // must be 0 for FAT32 uint16_t fatSize16; memcpy(&fatSize16, bpb + 22, 2); if (fatSize16 != 0) return nullptr; // must be 0 for FAT32 uint32_t fatSize32; memcpy(&fatSize32, bpb + 36, 4); if (fatSize32 == 0) return nullptr; uint32_t rootCluster; memcpy(&rootCluster, bpb + 44, 4); if (rootCluster < 2) return nullptr; uint16_t totalSectors16; memcpy(&totalSectors16, bpb + 19, 2); uint32_t totalSectors32; memcpy(&totalSectors32, bpb + 32, 4); uint32_t totalSectors = (totalSectors16 != 0) ? totalSectors16 : totalSectors32; if (totalSectors == 0) return nullptr; // Compute geometry uint32_t dataStartSector = reservedSectors + numFats * fatSize32; uint32_t dataSectors = totalSectors - dataStartSector; uint32_t clusterCount = dataSectors / sectorsPerCluster; // FAT32 requires >= 65525 clusters if (clusterCount < 65525) return nullptr; // Check FS type string for extra confidence (offset 82, "FAT32 ") // Not strictly required by spec, but good sanity check bool hasFat32Str = (bpb[82] == 'F' && bpb[83] == 'A' && bpb[84] == 'T' && bpb[85] == '3' && bpb[86] == '2'); // At least one of: valid cluster count or FS type string if (!hasFat32Str && clusterCount < 65525) return nullptr; // Success — initialize instance int idx = g_instanceCount; auto& inst = g_instances[idx]; inst.active = true; inst.blockDevIndex = blockDevIndex; inst.partStartLba = startLba; inst.bytesPerSector = bytesPerSector; inst.sectorsPerCluster = sectorsPerCluster; inst.reservedSectors = reservedSectors; inst.numFats = numFats; inst.fatSize32 = fatSize32; inst.rootCluster = rootCluster; inst.totalSectors = totalSectors; inst.clusterSize = (uint32_t)bytesPerSector * sectorsPerCluster; inst.dataStartSector = dataStartSector; inst.clusterCount = clusterCount; // Volume label (offset 71, 11 bytes) memcpy(inst.volumeLabel, bpb + 71, 11); inst.volumeLabel[11] = '\0'; // Trim trailing spaces for (int i = 10; i >= 0; i--) { if (inst.volumeLabel[i] == ' ') inst.volumeLabel[i] = '\0'; else break; } // Allocate cluster buffer inst.clusterBufPages = ((int)inst.clusterSize + 0xFFF) / 0x1000; if (inst.clusterBufPages == 1) { inst.clusterBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed(); } else { inst.clusterBuf = (uint8_t*)Memory::g_pfa->ReallocConsecutive( nullptr, inst.clusterBufPages); } // Load entire FAT into memory for fast cluster chain lookups. // fatSize32 is in sectors; each sector is bytesPerSector bytes. uint64_t fatBytes = (uint64_t)fatSize32 * bytesPerSector; inst.fatCachePages = (int)((fatBytes + 0xFFF) / 0x1000); inst.fatCacheEntries = (uint32_t)(fatBytes / 4); inst.fatCache = (uint32_t*)Memory::g_pfa->ReallocConsecutive( nullptr, inst.fatCachePages); if (inst.fatCache) { uint8_t* dst = (uint8_t*)inst.fatCache; uint64_t remaining = fatBytes; uint64_t fatPartSector = reservedSectors; while (remaining > 0) { uint32_t chunk = (remaining > 4096) ? 4096 : (uint32_t)remaining; uint32_t secs = (chunk + bytesPerSector - 1) / bytesPerSector; if (!ReadPartSectors(inst, fatPartSector, secs, dst)) { // If read fails, disable cache and fall back to per-lookup reads inst.fatCache = nullptr; break; } dst += secs * bytesPerSector; fatPartSector += secs; remaining -= secs * bytesPerSector; } } // Clear file handles for (int i = 0; i < MaxFilesPerInstance; i++) { inst.files[i].inUse = false; } g_instanceCount++; KernelLogStream(OK, "FAT32") << "Mounted volume \"" << inst.volumeLabel << "\" (" << clusterCount << " clusters, " << (uint64_t)inst.clusterSize << " bytes/cluster)"; return &g_drivers[idx]; } void RegisterProbe() { FsProbe::Register(Mount); } // ========================================================================= // FAT32 Format // ========================================================================= int Format(int blockDevIndex, uint64_t startLba, uint64_t sectorCount, const char* volumeLabel) { auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex); if (!dev) return -1; // FAT32 requires at least 65525 clusters. With 8 sectors/cluster (4K), // minimum is ~32MB. Reject anything too small. if (sectorCount < 2048) { KernelLogStream(ERROR, "FAT32") << "Partition too small to format as FAT32"; return -1; } // Geometry parameters uint16_t bytesPerSector = 512; uint8_t sectorsPerCluster; // Choose cluster size based on volume size uint64_t sizeMB = (sectorCount * bytesPerSector) / (1024 * 1024); if (sizeMB < 256) sectorsPerCluster = 1; // 512B clusters else if (sizeMB < 8192) sectorsPerCluster = 8; // 4K clusters else if (sizeMB < 16384) sectorsPerCluster = 16; // 8K clusters else if (sizeMB < 32768) sectorsPerCluster = 32; // 16K clusters else sectorsPerCluster = 64; // 32K clusters uint16_t reservedSectors = 32; uint8_t numFats = 2; // Compute FAT size using the standard formula: // fatSize = ceil((totalSectors - reservedSectors) / (spc * 128 + numFats)) // where 128 = bytesPerSector / 4 (FAT32 entries per sector) uint32_t totalSectors = (uint32_t)sectorCount; uint32_t denom = (uint32_t)sectorsPerCluster * 128 + numFats; uint32_t numer = totalSectors - reservedSectors; uint32_t fatSize = (numer + denom - 1) / denom; uint32_t dataStart = reservedSectors + numFats * fatSize; uint32_t dataSectors = totalSectors - dataStart; uint32_t clusterCount = dataSectors / sectorsPerCluster; if (clusterCount < 65525) { KernelLogStream(ERROR, "FAT32") << "Cluster count " << (uint64_t)clusterCount << " too low for FAT32 (need >= 65525)"; return -1; } uint32_t rootCluster = 2; // first data cluster // --- Build boot sector (BPB) --- uint8_t bpb[512]; memset(bpb, 0, 512); // Jump instruction bpb[0] = 0xEB; bpb[1] = 0x58; bpb[2] = 0x90; // OEM name memcpy(bpb + 3, "MNTK ", 8); // BPB fields memcpy(bpb + 11, &bytesPerSector, 2); bpb[13] = sectorsPerCluster; memcpy(bpb + 14, &reservedSectors, 2); bpb[16] = numFats; // rootEntryCount = 0 (FAT32) // totalSectors16 = 0 (FAT32) bpb[21] = 0xF8; // media type: hard disk // fatSize16 = 0 (FAT32) // Sectors per track / heads (for CHS, not critical) uint16_t spt = 63; memcpy(bpb + 24, &spt, 2); uint16_t heads = 255; memcpy(bpb + 26, &heads, 2); // Hidden sectors (LBA of partition start) uint32_t hidden = (uint32_t)startLba; memcpy(bpb + 28, &hidden, 4); // Total sectors 32 memcpy(bpb + 32, &totalSectors, 4); // FAT32-specific fields memcpy(bpb + 36, &fatSize, 4); // Ext flags = 0, FS version = 0 memcpy(bpb + 44, &rootCluster, 4); uint16_t fsInfoSector = 1; memcpy(bpb + 48, &fsInfoSector, 2); uint16_t backupBootSector = 6; memcpy(bpb + 50, &backupBootSector, 2); // Extended boot record bpb[64] = 0x80; // drive number bpb[66] = 0x29; // extended boot signature // Volume serial (from RDTSC) uint32_t lo, hi; asm volatile ("rdtsc" : "=a"(lo), "=d"(hi)); uint32_t serial = lo ^ hi; memcpy(bpb + 67, &serial, 4); // Volume label (11 bytes, space-padded) char label[11]; memset(label, ' ', 11); if (volumeLabel) { for (int i = 0; i < 11 && volumeLabel[i]; i++) { char ch = volumeLabel[i]; if (ch >= 'a' && ch <= 'z') ch -= 32; // uppercase label[i] = ch; } } else { memcpy(label, "NO NAME ", 11); } memcpy(bpb + 71, label, 11); // FS type memcpy(bpb + 82, "FAT32 ", 8); // Boot signature bpb[510] = 0x55; bpb[511] = 0xAA; // --- Write boot sector --- if (!dev->WriteSectors(dev->Ctx, startLba, 1, bpb)) { KernelLogStream(ERROR, "FAT32") << "Failed to write boot sector"; return -1; } // --- Write backup boot sector at sector 6 --- if (!dev->WriteSectors(dev->Ctx, startLba + backupBootSector, 1, bpb)) { KernelLogStream(ERROR, "FAT32") << "Failed to write backup boot sector"; return -1; } // --- Build and write FSInfo sector --- uint8_t fsinfo[512]; memset(fsinfo, 0, 512); uint32_t fsInfoSig1 = 0x41615252; memcpy(fsinfo + 0, &fsInfoSig1, 4); uint32_t fsInfoSig2 = 0x61417272; memcpy(fsinfo + 484, &fsInfoSig2, 4); uint32_t freeCount = clusterCount - 1; // minus root dir cluster memcpy(fsinfo + 488, &freeCount, 4); uint32_t nextFree = 3; // next free cluster after root memcpy(fsinfo + 492, &nextFree, 4); uint32_t fsInfoSig3 = 0xAA550000; memcpy(fsinfo + 508, &fsInfoSig3, 4); if (!dev->WriteSectors(dev->Ctx, startLba + 1, 1, fsinfo)) { KernelLogStream(ERROR, "FAT32") << "Failed to write FSInfo"; return -1; } // Backup FSInfo at sector 7 if (!dev->WriteSectors(dev->Ctx, startLba + 7, 1, fsinfo)) { KernelLogStream(ERROR, "FAT32") << "Failed to write backup FSInfo"; return -1; } // --- Zero out remaining reserved sectors --- uint8_t zeroBuf[512]; memset(zeroBuf, 0, 512); for (uint16_t s = 2; s < reservedSectors; s++) { if (s == backupBootSector || s == 7) continue; // already written dev->WriteSectors(dev->Ctx, startLba + s, 1, zeroBuf); } // --- Write FAT tables --- // First sector of each FAT: media byte + EOC for clusters 0,1 + EOC for root dir (cluster 2) uint8_t fatFirstSector[512]; memset(fatFirstSector, 0, 512); uint32_t fat0 = 0x0FFFFFF8; memcpy(fatFirstSector + 0, &fat0, 4); // cluster 0: media uint32_t fat1 = 0x0FFFFFFF; memcpy(fatFirstSector + 4, &fat1, 4); // cluster 1: EOC uint32_t fat2 = 0x0FFFFFFF; memcpy(fatFirstSector + 8, &fat2, 4); // cluster 2: root dir EOC for (int f = 0; f < numFats; f++) { uint64_t fatStart = startLba + reservedSectors + (uint64_t)f * fatSize; // Write first sector with media byte + root cluster entry if (!dev->WriteSectors(dev->Ctx, fatStart, 1, fatFirstSector)) { KernelLogStream(ERROR, "FAT32") << "Failed to write FAT " << f; return -1; } // Zero remaining FAT sectors for (uint32_t s = 1; s < fatSize; s++) { dev->WriteSectors(dev->Ctx, fatStart + s, 1, zeroBuf); } } // --- Zero root directory cluster --- uint64_t rootSector = startLba + dataStart; for (uint8_t s = 0; s < sectorsPerCluster; s++) { dev->WriteSectors(dev->Ctx, rootSector + s, 1, zeroBuf); } KernelLogStream(OK, "FAT32") << "Formatted: " << (uint64_t)clusterCount << " clusters, " << (uint64_t)sectorsPerCluster << " sec/cluster, FAT=" << (uint64_t)fatSize << " sectors"; return 0; } };