//! The on-disk layout of a FAT filesystem — the boot sector / BIOS Parameter //! Block, directory entries, long-file-name entries, and the FAT32 FSInfo — as //! `align(1)` extern structs that bit-cast straight out of a 512-byte sector //! (multi-byte fields are little-endian, like usb-abi.zig). Pure data, plus the //! cluster-count FAT-type detection. Host-testable. const std = @import("std"); /// The BIOS Parameter Block, common to FAT12/16/32 (offset 0..36 of the boot /// sector). The extended part that follows differs by FAT type. pub const BiosParameterBlock = extern struct { jump: [3]u8, oem_name: [8]u8, bytes_per_sector: u16 align(1), sectors_per_cluster: u8, reserved_sector_count: u16 align(1), fat_count: u8, root_entry_count: u16 align(1), total_sectors_16: u16 align(1), media: u8, fat_size_16: u16 align(1), sectors_per_track: u16 align(1), head_count: u16 align(1), hidden_sectors: u32 align(1), total_sectors_32: u32 align(1), }; /// The FAT12/16 extended boot record (offset 36). pub const ExtendedBootRecord16 = extern struct { drive_number: u8, reserved: u8, boot_signature: u8, volume_id: u32 align(1), volume_label: [11]u8, filesystem_type: [8]u8, }; /// The FAT32 extended boot record (offset 36). pub const ExtendedBootRecord32 = extern struct { fat_size_32: u32 align(1), extended_flags: u16 align(1), filesystem_version: u16 align(1), root_cluster: u32 align(1), filesystem_information_sector: u16 align(1), backup_boot_sector: u16 align(1), reserved: [12]u8, drive_number: u8, reserved1: u8, boot_signature: u8, volume_id: u32 align(1), volume_label: [11]u8, filesystem_type: [8]u8, }; /// A 32-byte directory entry (8.3 short name form). pub const DirectoryEntry = extern struct { name: [11]u8, // 8 name + 3 extension, space-padded attributes: u8, reserved_nt: u8, creation_time_tenth: u8, creation_time: u16 align(1), creation_date: u16 align(1), last_access_date: u16 align(1), first_cluster_high: u16 align(1), write_time: u16 align(1), write_date: u16 align(1), first_cluster_low: u16 align(1), file_size: u32 align(1), pub fn firstCluster(self: DirectoryEntry) u32 { return (@as(u32, self.first_cluster_high) << 16) | self.first_cluster_low; } pub fn setFirstCluster(self: *DirectoryEntry, cluster: u32) void { self.first_cluster_low = @truncate(cluster); self.first_cluster_high = @truncate(cluster >> 16); } pub fn isFree(self: DirectoryEntry) bool { return self.name[0] == 0x00 or self.name[0] == 0xE5; } pub fn isEnd(self: DirectoryEntry) bool { return self.name[0] == 0x00; } pub fn isDirectory(self: DirectoryEntry) bool { return self.attributes & attribute_directory != 0; } pub fn isLongName(self: DirectoryEntry) bool { return self.attributes & attribute_long_name_mask == attribute_long_name; } pub fn isVolumeLabel(self: DirectoryEntry) bool { return self.attributes & attribute_volume_id != 0 and !self.isLongName(); } }; /// A 32-byte long-file-name entry (attributes == 0x0F). A sequence of these /// precedes the 8.3 entry they name, each carrying 13 UTF-16 code units. pub const LongNameEntry = extern struct { order: u8, name1: [5]u16 align(1), attributes: u8, kind: u8, checksum: u8, name2: [6]u16 align(1), first_cluster_low: u16 align(1), name3: [2]u16 align(1), }; /// The FAT32 FSInfo sector (usually sector 1): advisory free-cluster bookkeeping. pub const FileSystemInformation = extern struct { lead_signature: u32 align(1), // 0x41615252 reserved1: [480]u8, struct_signature: u32 align(1), // 0x61417272 free_count: u32 align(1), next_free: u32 align(1), reserved2: [12]u8, trail_signature: u32 align(1), // 0xAA550000 }; // Directory-entry attribute bits. pub const attribute_read_only: u8 = 0x01; pub const attribute_hidden: u8 = 0x02; pub const attribute_system: u8 = 0x04; pub const attribute_volume_id: u8 = 0x08; pub const attribute_directory: u8 = 0x10; pub const attribute_archive: u8 = 0x20; pub const attribute_long_name: u8 = 0x0F; // read_only|hidden|system|volume_id pub const attribute_long_name_mask: u8 = 0x3F; // FSInfo signatures. pub const fsinfo_lead_signature: u32 = 0x41615252; pub const fsinfo_struct_signature: u32 = 0x61417272; pub const fsinfo_trail_signature: u32 = 0xAA550000; /// End-of-chain markers (a cluster value >= these ends a chain). pub const end_of_chain_12: u32 = 0xFF8; pub const end_of_chain_16: u32 = 0xFFF8; pub const end_of_chain_32: u32 = 0x0FFFFFF8; pub const bad_cluster_32: u32 = 0x0FFFFFF7; pub const free_cluster: u32 = 0; pub const boot_signature_offset: usize = 510; // 0x55 0xAA at the end of the boot sector pub const FatType = enum { fat12, fat16, fat32 }; /// The geometry derived from the BPB, plus the FAT type (by the Microsoft /// cluster-count rule: <4085 FAT12, <65525 FAT16, else FAT32). pub const Geometry = struct { fat_type: FatType, bytes_per_sector: u32, sectors_per_cluster: u32, reserved_sector_count: u32, fat_count: u32, fat_size_sectors: u32, // per FAT root_entry_count: u32, // FAT12/16 root_cluster: u32, // FAT32 first_data_sector: u32, total_sectors: u32, cluster_count: u32, fsinfo_sector: u32, // FAT32 }; /// Derive the geometry (and FAT type) from a boot sector's first 512 bytes. /// Returns null if the sector is not a plausible FAT boot sector. pub fn geometryOf(sector: []const u8) ?Geometry { if (sector.len < 512) return null; if (sector[boot_signature_offset] != 0x55 or sector[boot_signature_offset + 1] != 0xAA) return null; const bpb = std.mem.bytesToValue(BiosParameterBlock, sector[0..@sizeOf(BiosParameterBlock)]); if (bpb.bytes_per_sector == 0 or bpb.sectors_per_cluster == 0 or bpb.fat_count == 0) return null; const fat_size_16: u32 = bpb.fat_size_16; var fat_size: u32 = fat_size_16; var root_cluster: u32 = 0; var fsinfo_sector: u32 = 0; if (fat_size_16 == 0) { const ebr = std.mem.bytesToValue(ExtendedBootRecord32, sector[36 .. 36 + @sizeOf(ExtendedBootRecord32)]); fat_size = ebr.fat_size_32; root_cluster = ebr.root_cluster; fsinfo_sector = ebr.filesystem_information_sector; } const total_sectors: u32 = if (bpb.total_sectors_16 != 0) bpb.total_sectors_16 else bpb.total_sectors_32; const root_dir_sectors = (@as(u32, bpb.root_entry_count) * 32 + bpb.bytes_per_sector - 1) / bpb.bytes_per_sector; const first_data_sector = bpb.reserved_sector_count + bpb.fat_count * fat_size + root_dir_sectors; if (total_sectors < first_data_sector) return null; const data_sectors = total_sectors - first_data_sector; const cluster_count = data_sectors / bpb.sectors_per_cluster; const fat_type: FatType = if (cluster_count < 4085) .fat12 else if (cluster_count < 65525) .fat16 else .fat32; return .{ .fat_type = fat_type, .bytes_per_sector = bpb.bytes_per_sector, .sectors_per_cluster = bpb.sectors_per_cluster, .reserved_sector_count = bpb.reserved_sector_count, .fat_count = bpb.fat_count, .fat_size_sectors = fat_size, .root_entry_count = bpb.root_entry_count, .root_cluster = root_cluster, .first_data_sector = first_data_sector, .total_sectors = total_sectors, .cluster_count = cluster_count, .fsinfo_sector = fsinfo_sector, }; } test "on-disk struct sizes match the specification" { try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock)); try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16)); try std.testing.expectEqual(@as(usize, 54), @sizeOf(ExtendedBootRecord32)); try std.testing.expectEqual(@as(usize, 32), @sizeOf(DirectoryEntry)); try std.testing.expectEqual(@as(usize, 32), @sizeOf(LongNameEntry)); try std.testing.expectEqual(@as(usize, 512), @sizeOf(FileSystemInformation)); } test "directory entry cluster split/join" { var entry = std.mem.zeroes(DirectoryEntry); entry.setFirstCluster(0x01234567); try std.testing.expectEqual(@as(u16, 0x4567), entry.first_cluster_low); try std.testing.expectEqual(@as(u16, 0x0123), entry.first_cluster_high); try std.testing.expectEqual(@as(u32, 0x01234567), entry.firstCluster()); }