Loading learning content...
Every file you've ever saved has a name, a size, a creation date, and attributes like "read-only" or "hidden." On a FAT volume, all of this metadata lives in directory entries—fixed-size records that describe every file and subdirectory.
Directory entries are the bridge between human concepts ("My vacation photos are in Documents/Pictures/Hawaii2024") and machine storage ("The file's data starts at cluster 58,421"). Understanding their structure reveals how file systems translate our organizational metaphors into disk reality.
This page dissects the 32-byte directory entry format that has stored file metadata since the early days of MS-DOS—and the clever extensions that later added support for long filenames.
By the end of this page, you will understand the complete FAT directory entry structure, including the 8.3 filename format, file attributes, timestamp encoding, long filename (LFN) entries, and how directories organize their contents. You'll be able to read and interpret raw directory entry bytes.
The original FAT directory entry was designed in 1980 for MS-DOS. It allocated exactly 11 bytes for filenames: 8 characters for the name and 3 for the extension. This is the infamous 8.3 format.
Format Rules:
123456789101112
User-entered filename Stored as (11 bytes)------------------------ -------------------------"HELLO.TXT" → "HELLO TXT" (HELLO + 3 spaces + TXT)"A.B" → "A B " (A + 7 spaces + B + 2 spaces)"README" → "README " (README + 2 spaces + 3 spaces)"IO.SYS" → "IO SYS" (IO + 6 spaces + SYS)"12345678.123" → "12345678123" (Maximum length) Illegal transformations (not possible in pure 8.3):"My Document.txt" → Too long, requires LFN"report.2024.txt" → Multiple dots, requires LFN"résumé.doc" → Extended characters, requires LFNSpecial First-Byte Values:
The first byte of the filename has special meanings:
| Value | Meaning | Notes |
|---|---|---|
| 0x00 | Entry never used | End of directory; stop searching |
| 0xE5 | Deleted entry | Entry was deleted; can be reused |
| 0x05 | Actual 0xE5 character | Kanji lead byte workaround |
| 0x2E | Dot entry | '. ' or '.. ' for current/parent directory |
When a file is deleted, only the first byte of its name is changed to 0xE5. The rest of the metadata (including the first cluster and file size) remains intact until overwritten. This is why file recovery tools can often restore recently deleted files—they find entries starting with 0xE5 and attempt to reconstruct the cluster chain.
Each directory entry occupies exactly 32 bytes. This fixed size simplifies directory handling but limits the information that can be stored.
Complete Directory Entry Layout:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
typedef struct __attribute__((packed)) { // Offset 0x00: Filename (8.3 format) uint8_t name[8]; // 0x00-0x07: Short filename (padded with spaces) uint8_t ext[3]; // 0x08-0x0A: Extension (padded with spaces) // Offset 0x0B: File attributes uint8_t attr; // 0x0B: Attribute byte (see bit definitions) // Offset 0x0C: Windows NT reserved / case information uint8_t ntReserved; // 0x0C: Case info for name/ext (Windows NT) // Offset 0x0D: Creation time fine resolution uint8_t createTimeTenth; // 0x0D: Creation time, 10ms resolution (0-199) // Offset 0x0E: Creation time and date uint16_t createTime; // 0x0E-0x0F: Creation time uint16_t createDate; // 0x10-0x11: Creation date // Offset 0x12: Last access date uint16_t accessDate; // 0x12-0x13: Last access date (date only) // Offset 0x14: High word of first cluster (FAT32 only) uint16_t firstClusterHigh; // 0x14-0x15: High 16 bits of cluster (FAT32) // Offset 0x16: Last modification time and date uint16_t modifyTime; // 0x16-0x17: Last modification time uint16_t modifyDate; // 0x18-0x19: Last modification date // Offset 0x1A: Low word of first cluster uint16_t firstClusterLow; // 0x1A-0x1B: Low 16 bits of first cluster // Offset 0x1C: File size uint32_t fileSize; // 0x1C-0x1F: File size in bytes} FATDirectoryEntry; // Total: 32 bytes exactly // Attribute byte bit definitions#define ATTR_READ_ONLY 0x01 // File is read-only#define ATTR_HIDDEN 0x02 // File is hidden#define ATTR_SYSTEM 0x04 // System file#define ATTR_VOLUME_ID 0x08 // Volume label entry#define ATTR_DIRECTORY 0x10 // Entry is a directory#define ATTR_ARCHIVE 0x20 // Archive flag (file was modified) // Special attribute combination for LFN entries#define ATTR_LONG_NAME 0x0F // (READ_ONLY | HIDDEN | SYSTEM | VOLUME_ID)#define ATTR_LONG_NAME_MASK 0x3FPractical Example:
Let's decode a real directory entry:
123456789101112131415161718192021222324
Raw bytes (32 bytes, hex):52 45 41 44 4D 45 20 20 54 58 54 20 18 00 00 0000 00 00 00 00 00 8A 6D 59 58 02 00 2A 01 00 00 Decoded:Offset Bytes Meaning------ -------------- -------------------------------0x00 52 45 41 44 'READ' (first 4 chars of name)0x04 4D 45 20 20 'ME ' (remaining name + padding)0x08 54 58 54 'TXT' (extension)0x0B 20 Attributes: 0x20 = ARCHIVE0x0C 18 NT Reserved/case: lowercase name0x0D 00 Create time 10ths: 00x0E 00 00 Create time: 0 (not set)0x10 00 00 Create date: 0 (not set)0x12 00 00 Access date: 0 (not set)0x14 00 00 First cluster high: 0 (FAT16)0x16 8A 6D Modify time: 13:44:200x18 59 58 Modify date: 2024-02-250x1A 02 00 First cluster low: 20x1C 2A 01 00 00 File size: 298 bytes Result: "README.TXT", 298 bytes, starts at cluster 2, modified 2024-02-25 13:44:20, archivedThe single attribute byte packs significant meaning into 8 bits. Each bit represents a specific file property:
| Bit | Hex | Name | Meaning | Common Use |
|---|---|---|---|---|
| 0 | 0x01 | Read-Only | File cannot be written or deleted | Protect important files |
| 1 | 0x02 | Hidden | File hidden from normal directory listings | System files, user preference |
| 2 | 0x04 | System | Operating system file | Kernel, drivers, config |
| 3 | 0x08 | Volume ID | Entry is the volume label, not a file | Exactly one per volume root |
| 4 | 0x10 | Directory | Entry is a subdirectory, not a file | Folders |
| 5 | 0x20 | Archive | File has been modified since last backup | Incremental backup systems |
| 6-7 | — | Reserved | Must be zero | — |
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Check if entry is a directorybool isDirectory(FATDirectoryEntry *entry) { return (entry->attr & ATTR_DIRECTORY) != 0;} // Check if entry is a regular file (not dir, volume label, or LFN)bool isRegularFile(FATDirectoryEntry *entry) { return (entry->attr & (ATTR_DIRECTORY | ATTR_VOLUME_ID | ATTR_LONG_NAME_MASK)) == 0 || (entry->attr & ATTR_ARCHIVE);} // Check if entry is a Long Filename (LFN) componentbool isLFNEntry(FATDirectoryEntry *entry) { return (entry->attr & ATTR_LONG_NAME_MASK) == ATTR_LONG_NAME;} // Check if file is hidden from normal listingsbool isHidden(FATDirectoryEntry *entry) { return (entry->attr & ATTR_HIDDEN) != 0;} // Check if file should be excluded from backupbool needsBackup(FATDirectoryEntry *entry) { return (entry->attr & ATTR_ARCHIVE) != 0;} // Mark file as backed up (clear archive flag)void markBackedUp(FATDirectoryEntry *entry) { entry->attr &= ~ATTR_ARCHIVE;} // Set file as modified (set archive flag)void markModified(FATDirectoryEntry *entry) { entry->attr |= ATTR_ARCHIVE;} // Common attribute combinations:// 0x00 - Normal file// 0x01 - Read-only file// 0x02 - Hidden file// 0x04 - System file// 0x06 - Hidden system file// 0x07 - Read-only hidden system file (e.g., io.sys)// 0x10 - Directory// 0x20 - Normal file (archive bit set after modification)The archive bit is automatically set by operating systems whenever a file is created or modified. Backup software clears this bit after copying the file. This allows incremental backups—only files with the archive bit set have changed since the last backup.
FAT stores three timestamps: creation, modification, and last access. Due to space constraints, these use a compact binary encoding.
Time Format (16 bits):
1234567891011121314
16-bit time format:┌─────────────────┬─────────────────┬─────────────────┐│ Bits 15-11 (5) │ Bits 10-5 (6) │ Bits 4-0 (5) ││ Hours (0-23) │ Minutes (0-59) │ Seconds/2 (0-29)│└─────────────────┴─────────────────┴─────────────────┘ Note: Seconds are divided by 2, giving 2-second resolution. To get actual seconds: (bits 4-0) × 2 Example: 0x6D8A = 0110 1101 1000 1010 Hours: 01101 = 13 Minutes: 101100 = 44 Seconds: 01010 = 10 × 2 = 20 Result: 13:44:20Date Format (16 bits):
1234567891011121314
16-bit date format:┌─────────────────┬─────────────────┬─────────────────┐│ Bits 15-9 (7) │ Bits 8-5 (4) │ Bits 4-0 (5) ││ Year since 1980 │ Month (1-12) │ Day (1-31) │└─────────────────┴─────────────────┴─────────────────┘ Year is offset from 1980 (0 = 1980, 127 = 2107).Valid range: 1980-01-01 to 2107-12-31 Example: 0x5859 = 0101 1000 0101 1001 Year: 0101100 = 44 → 1980 + 44 = 2024 Month: 0010 = 2 (February) Day: 11001 = 25 Result: 2024-02-251234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// Decode FAT timestamp to componentsvoid decodeFATDateTime(uint16_t time, uint16_t date, int *year, int *month, int *day, int *hour, int *minute, int *second) { // Decode date *year = ((date >> 9) & 0x7F) + 1980; *month = (date >> 5) & 0x0F; *day = date & 0x1F; // Decode time *hour = (time >> 11) & 0x1F; *minute = (time >> 5) & 0x3F; *second = (time & 0x1F) * 2; // Note: multiply by 2} // Encode components to FAT timestampvoid encodeFATDateTime(int year, int month, int day, int hour, int minute, int second, uint16_t *time, uint16_t *date) { // Clamp year to valid range if (year < 1980) year = 1980; if (year > 2107) year = 2107; // Encode date *date = ((year - 1980) << 9) | (month << 5) | day; // Encode time (round seconds down to even) *time = (hour << 11) | (minute << 5) | (second / 2);} // Get current time as FAT timestampvoid getCurrentFATTimestamp(uint16_t *time, uint16_t *date) { time_t now = time(NULL); struct tm *tm = localtime(&now); encodeFATDateTime( tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, time, date );} // Creation time has additional 10ms resolution// The createTimeTenth field stores 0-199 (tenth of seconds 0-19.9)int getCreationMilliseconds(FATDirectoryEntry *entry) { return (entry->createTimeTenth % 100) * 10; // 0-990 ms} int getCreationSeconds(FATDirectoryEntry *entry) { int baseSec = (entry->createTime & 0x1F) * 2; int extraSec = entry->createTimeTenth / 100; // 0-1 extra second return baseSec + extraSec;}FAT's date format can only represent years 1980-2107 (7 bits for year offset). While this seems distant, archived storage may still exist by then. This is analogous to Y2K but for FAT file systems. Long-term archival formats should consider this limitation.
The 8.3 filename format was painfully limiting. In Windows 95, Microsoft introduced Long Filename (LFN) support while maintaining backward compatibility. The clever solution: store long names in multiple special directory entries that older systems would ignore.
LFN Design Principles:
123456789101112131415161718192021222324252627282930
typedef struct __attribute__((packed)) { uint8_t ordinal; // 0x00: Sequence number (1-20, 0x40 OR'd for last) uint16_t name1[5]; // 0x01-0x0A: Characters 1-5 (Unicode) uint8_t attr; // 0x0B: Always 0x0F (ATTR_LONG_NAME) uint8_t type; // 0x0C: Always 0x00 for LFN uint8_t checksum; // 0x0D: Checksum of 8.3 name uint16_t name2[6]; // 0x0E-0x19: Characters 6-11 (Unicode) uint16_t cluster; // 0x1A-0x1B: Always 0x0000 uint16_t name3[2]; // 0x1C-0x1F: Characters 12-13 (Unicode)} LFNEntry; // 13 Unicode characters per LFN entry:// name1[5] + name2[6] + name3[2] = 13 characters // Maximum filename: 20 LFN entries × 13 = 260 characters// (Actually limited to 255 by Windows) // Ordinal field:// - Bits 0-5: Sequence number (1 = first part of name)// - Bit 6 (0x40): Set if this is the LAST (first on disk) entry// - Bit 7: Deleted flag (0xE5 hack uses different position) // Example: "My Vacation Photos 2024.jpg"// Length: 27 characters → needs ceil(27/13) = 3 LFN entries // On-disk order (LFN entries stored in REVERSE):// Entry 1: LFN ordinal 0x43 (last, sequence 3) - "4.jpg\0\xFF\xFF..."// Entry 2: LFN ordinal 0x02 (sequence 2) - "ion Photos 202"// Entry 3: LFN ordinal 0x01 (sequence 1) - "My Vacat"// Entry 4: Standard 8.3 entry - "MYVACA~1.JPG"The 8.3 Short Name Generation:
Even with LFN, every file needs a valid 8.3 name for backward compatibility:
12345678910111213141516171819
Short name generation (simplified Windows algorithm): 1. Convert to uppercase2. Remove invalid characters (spaces, most punctuation)3. Take first 6 valid characters of base name4. Append "~1" (or ~2, ~3... if collision)5. Take first 3 characters of extension Examples:Long Name → Short Name----------------------------- ---------------"My Document.txt" → "MYDOCU~1.TXT""My Document (copy).txt" → "MYDOCU~2.TXT" (if ~1 exists)"Very Long Filename Here.pdf"→ "VERYLO~1.PDF""Report.2024.Final.docx" → "REPORT~1.DOC" (one extension)"日本語ファイル.txt" → "____~1.TXT" (Unicode stripped) If many collisions, switch to hash-based format:"VERYL~B7.PDF" (B7 = hash of original name)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Calculate checksum of 8.3 name for LFN validationuint8_t calculateLFNChecksum(uint8_t *shortName) { uint8_t sum = 0; for (int i = 0; i < 11; i++) { // Rotate right and add sum = ((sum & 1) ? 0x80 : 0) + (sum >> 1) + shortName[i]; } return sum;} // Reconstruct long filename from LFN entriesint extractLongFilename(LFNEntry *lfnEntries, int count, uint16_t *outName, int maxLen) { int pos = 0; // LFN entries are stored in reverse order on disk // Entry with 0x40 flag is last (contains end of name) for (int i = count - 1; i >= 0 && pos < maxLen; i--) { LFNEntry *e = &lfnEntries[i]; // Validate LFN attribute if ((e->attr & ATTR_LONG_NAME_MASK) != ATTR_LONG_NAME) { return -1; // Not an LFN entry } // Copy characters from name1, name2, name3 for (int j = 0; j < 5 && pos < maxLen; j++) { if (e->name1[j] == 0x0000 || e->name1[j] == 0xFFFF) goto done; outName[pos++] = e->name1[j]; } for (int j = 0; j < 6 && pos < maxLen; j++) { if (e->name2[j] == 0x0000 || e->name2[j] == 0xFFFF) goto done; outName[pos++] = e->name2[j]; } for (int j = 0; j < 2 && pos < maxLen; j++) { if (e->name3[j] == 0x0000 || e->name3[j] == 0xFFFF) goto done; outName[pos++] = e->name3[j]; } } done: outName[pos] = 0; // Null terminate return pos;}The reason older systems ignore LFN entries is the attribute byte 0x0F. This combination (Read-Only + Hidden + System + Volume Label) is invalid for regular files, so old FAT code skips these entries entirely. It's a brilliant backward-compatible hack.
Root Directory:
The root directory is special:
1234567891011121314151617181920212223
// Calculate root directory locationvoid getRootDirectoryLocation(FatType type, uint32_t *sector, uint32_t *size) { if (type == FAT32) { // FAT32: Root is a cluster chain starting at rootCluster // Use cluster traversal functions *sector = clusterToSector(bpb32->rootCluster); *size = 0; // Variable size, follow chain } else { // FAT12/FAT16: Fixed location after FAT tables uint32_t fatSize = bpb->sectorsPerFAT16 * bpb->numberOfFATs; *sector = bpb->reservedSectors + fatSize; // Size in sectors *size = (bpb->rootEntryCount * 32 + bytesPerSector - 1) / bytesPerSector; }} // Typical FAT16 root: 512 entries × 32 bytes = 16,384 bytes = 32 sectors// This limits FAT16 root to 512 files/directories! // FAT32 root: No limit (as many clusters as needed)Subdirectories:
Subdirectories are files with the DIRECTORY attribute. Their "file data" is a sequence of directory entries:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// When a subdirectory is created:// 1. Allocate at least one cluster for the directory// 2. Create two special entries: "." and ".." int createSubdirectory(DirectoryEntry *parentEntry, const char *name) { // Allocate cluster for new directory uint32_t cluster = allocateCluster(); if (cluster == 0) return -ENOSPC; // Zero the cluster zeroCluster(cluster); // Create "." entry (points to self) DirectoryEntry dot = {0}; memcpy(dot.name, ". ", 11); // ". " padded dot.attr = ATTR_DIRECTORY; dot.firstClusterLow = cluster & 0xFFFF; dot.firstClusterHigh = cluster >> 16; setCurrentTimestamp(&dot); writeDirectoryEntry(cluster, 0, &dot); // Create ".." entry (points to parent) DirectoryEntry dotdot = {0}; memcpy(dotdot.name, ".. ", 11); // ".. " padded dotdot.attr = ATTR_DIRECTORY; uint32_t parentCluster = getFirstCluster(parentEntry); // Root directory has ".." pointing to cluster 0 if (parentCluster == bpb32->rootCluster) parentCluster = 0; dotdot.firstClusterLow = parentCluster & 0xFFFF; dotdot.firstClusterHigh = parentCluster >> 16; setCurrentTimestamp(&dotdot); writeDirectoryEntry(cluster, 1, &dotdot); // Create entry in parent directory DirectoryEntry newDir = {0}; formatShortName(newDir.name, name); newDir.attr = ATTR_DIRECTORY; newDir.firstClusterLow = cluster & 0xFFFF; newDir.firstClusterHigh = cluster >> 16; setCurrentTimestamp(&newDir); addEntryToDirectory(parentEntry, &newDir); return 0;} // Subdirectory contents:// Offset Entry// 0 "." (current directory)// 1 ".." (parent directory)// 2+ Regular file/directory entries// ...// N 0x00 (end marker)Every subdirectory begins with '.' and '..' entries. The '.' entry's first cluster points to the directory itself, enabling programs to get the current directory's cluster. The '..' entry points to the parent directory, enabling 'cd ..' navigation. In the root directory, '..' has first cluster 0.
Let's examine the core operations on directories:
Finding a File:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
// Look up a file in a directory by name// Returns entry pointer or NULL if not foundDirectoryEntry *findFileInDirectory(uint32_t dirCluster, const char *name) { static DirectoryEntry result; uint8_t buffer[CLUSTER_SIZE]; // Generate 8.3 name for comparison char shortName[12]; formatShortName(shortName, name); // For LFN matching, convert to Unicode uint16_t longNameUnicode[256]; stringToUnicode(name, longNameUnicode, 256); // LFN accumulation uint16_t lfnBuffer[256]; int lfnPos = 0; uint8_t lfnChecksum = 0; // Iterate through directory clusters uint32_t cluster = dirCluster; while (!isEndOfChain(cluster)) { readCluster(cluster, buffer); // Process each entry in cluster for (int i = 0; i < ENTRIES_PER_CLUSTER; i++) { DirectoryEntry *entry = (DirectoryEntry *)(buffer + i * 32); // End of directory? if (entry->name[0] == 0x00) { return NULL; } // Skip deleted entries if (entry->name[0] == 0xE5) { lfnPos = 0; // Reset LFN accumulation continue; } // LFN entry? if ((entry->attr & ATTR_LONG_NAME_MASK) == ATTR_LONG_NAME) { LFNEntry *lfn = (LFNEntry *)entry; // First LFN entry (last in sequence) starts fresh if (lfn->ordinal & 0x40) { lfnPos = 0; lfnChecksum = lfn->checksum; } // Accumulate LFN characters // (entries are in reverse order, handle accordingly) accumulateLFN(lfn, lfnBuffer, &lfnPos); continue; } // Regular entry - check for match // First, compare short name if (memcmp(entry->name, shortName, 11) == 0) { memcpy(&result, entry, 32); return &result; } // If we accumulated LFN, verify and compare if (lfnPos > 0) { if (calculateLFNChecksum(entry->name) == lfnChecksum) { lfnBuffer[lfnPos] = 0; // Null terminate if (unicodeCaseCompare(lfnBuffer, longNameUnicode)) { memcpy(&result, entry, 32); return &result; } } } lfnPos = 0; // Reset for next file } cluster = getFATEntry(cluster); } return NULL; // Not found}Creating a New File:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
// Create a new file entry in a directoryint createFileInDirectory(uint32_t dirCluster, const char *name, uint8_t attr, DirectoryEntry *outEntry) { // Check if file already exists if (findFileInDirectory(dirCluster, name) != NULL) { return -EEXIST; } // Determine if we need LFN entries char shortName[12]; int needsLFN = !canBeShortName(name) || shortNameCollides(dirCluster, name, shortName); int lfnCount = 0; LFNEntry lfnEntries[20]; if (needsLFN) { // Generate short name with numeric tail generateUniqueShortName(dirCluster, name, shortName); // Create LFN entries lfnCount = createLFNEntries(name, shortName, lfnEntries); } else { formatShortName(shortName, name); } // Find consecutive free slots (lfnCount + 1 for main entry) int slotsNeeded = lfnCount + 1; uint32_t slot = findFreeSlots(dirCluster, slotsNeeded); if (slot == (uint32_t)-1) { // Need to extend directory if (!extendDirectory(dirCluster)) { return -ENOSPC; } slot = findFreeSlots(dirCluster, slotsNeeded); } // Write LFN entries (in reverse order) for (int i = 0; i < lfnCount; i++) { writeDirectorySlot(dirCluster, slot + lfnCount - 1 - i, &lfnEntries[i]); } // Create and write main entry DirectoryEntry entry = {0}; memcpy(entry.name, shortName, 11); entry.attr = attr; setCurrentTimestamp(&entry); entry.firstClusterLow = 0; // No data yet entry.firstClusterHigh = 0; entry.fileSize = 0; writeDirectorySlot(dirCluster, slot + lfnCount, &entry); if (outEntry) { memcpy(outEntry, &entry, 32); } return 0;}Directory entries are the essential metadata layer connecting human-readable file organization to physical cluster storage. Let's consolidate:
What's next:
We've now covered FAT's core components: the FAT table, FAT variants, cluster chains, and directory entries. The final page examines FAT's limitations—the constraints that eventually led to more advanced file systems, and why FAT remains in use despite these shortcomings.
You now understand FAT directory entries—from the 32-byte structure and 8.3 naming to LFN support and directory organization. This knowledge enables you to read, interpret, and manipulate FAT metadata directly. Next, we'll explore FAT's limitations.