{"record":{"id":"0f1ff59224370e4c","repo":"felixrieseberg/windows95","slug":"unexpected-sector-size","errorCode":null,"errorMessage":"unexpected sector size","messagePattern":"unexpected sector size","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/renderer/utils/fat32-extract.ts","lineNumber":29,"sourceCode":" * the output isn't 200 MB of possibly-mismatched OS binaries.\n */\nexport async function extractFat32(\n  readSector: (lba: number) => Buffer,\n  isDirty: (lba: number) => boolean,\n  outDir: string,\n): Promise<number> {\n  // First partition from the MBR.\n  const mbr = readSector(0);\n  const partLba = mbr.readUInt32LE(0x1be + 8);\n\n  const bpb = readSector(partLba);\n  const bytesPerSec = bpb.readUInt16LE(11);\n  const secPerClus = bpb.readUInt8(13);\n  const rsvd = bpb.readUInt16LE(14);\n  const nFats = bpb.readUInt8(16);\n  const secPerFat = bpb.readUInt32LE(36);\n  const rootClus = bpb.readUInt32LE(44);\n  if (bytesPerSec !== 512) throw new Error(\"unexpected sector size\");\n\n  const fatLba = partLba + rsvd;\n  const dataLba = partLba + rsvd + nFats * secPerFat;\n  const clusLba = (c: number) => dataLba + (c - 2) * secPerClus;\n\n  const fatSecCache = new Map<number, Buffer>();\n  const nextCluster = (c: number) => {\n    const off = c * 4;\n    const sec = fatLba + (off >> 9);\n    let b = fatSecCache.get(sec);\n    if (!b) fatSecCache.set(sec, (b = readSector(sec)));\n    return b.readUInt32LE(off & 511) & 0x0fffffff;\n  };\n\n  const chain = (c: number) => {\n    const out: number[] = [];\n    while (c >= 2 && c < 0x0ffffff8 && out.length < 1 << 20) {\n      out.push(c);","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/felixrieseberg/windows95/blob/051065e5ae815203a0a9038e01eefdf3c10519f1/src/renderer/utils/fat32-extract.ts#L11-L47","documentation":"extractFat32 is a minimal read-only FAT32 walker that hard-codes a 512-byte sector size in every offset calculation (FAT entry offsets use `off & 511`, cluster buffers use `secPerClus * 512`, and the sector reader contract is 512 bytes per LBA). Before parsing any directory or FAT structure it validates the BPB's bytes-per-sector field (UInt16LE at offset 11 of the partition's boot sector). If the volume was not formatted with 512-byte sectors, the walker's arithmetic would silently read garbage, so it throws this error defensively.","triggerScenarios":"Calling extractFat32 (directly or via recoverLegacyDisk) against a disk image whose first MBR partition's BPB declares bytesPerSec != 512 — e.g. a 1024- or 2048-byte-sector FAT32 volume, or the partition LBA resolved to a non-FAT region so offset 11 contains unrelated bytes.","commonSituations":"Extracting from disk images created for media/industrial devices that use 1K/2K sector emulation; pointing the extractor at an exFAT or non-FAT partition where the BPB layout doesn't apply; an MBR entry pointing to the wrong partition start (partLba) so a non-boot-sector buffer is parsed; re-purposing the walker on an image formatted with large-sector geometry.","solutions":["Verify the target partition is a standard 512-byte-sector FAT32 volume (run `fdisk -l` or inspect BPB offset 11; value must be 512).","If the image genuinely uses larger sectors, reformat or convert the image to 512-byte sectors (e.g. with a tool like `qemu-img convert` plus reformat) before extraction.","Check the MBR partition entry: confirm partLba (offset 0x1be+8) points at the FAT32 VBR, not some other partition or raw data.","If you control the image pipeline, format with `mkfs.vfat -S 512` or default settings to guarantee 512-byte sectors.","If you must support non-512 sectors, generalize the walker: replace all hardcoded 512/511 constants with the bytesPerSec value read from the BPB."],"exampleFix":"// before (fat32-extract.ts)\nif (bytesPerSec !== 512) throw new Error(\"unexpected sector size\");\n\n// after — caller validates the image before extraction:\nconst bpb = readSector(partLba);\nconst bytesPerSec = bpb.readUInt16LE(11);\nif (bytesPerSec !== 512) {\n  throw new Error(`image uses ${bytesPerSec}-byte sectors; reformat with 512-byte sectors before recovery`);\n}","handlingStrategy":"validation","validationCode":"import * as fs from \"fs\";\n\nexport function assertFat32SectorSize(imagePath: string): void {\n  const fd = fs.openSync(imagePath, \"r\");\n  try {\n    const mbr = Buffer.alloc(512);\n    fs.readSync(fd, mbr, 0, 512, 0);\n    const partLba = mbr.readUInt32LE(0x1be + 8);\n    if (partLba === 0) throw new Error(\"no MBR partition entry\");\n    const bpb = Buffer.alloc(512);\n    fs.readSync(fd, bpb, 0, 512, partLba * 512);\n    const bytesPerSec = bpb.readUInt16LE(11);\n    if (bytesPerSec !== 512) {\n      throw new Error(`partition uses ${bytesPerSec}-byte sectors; only 512 is supported`);\n    }\n  } finally {\n    fs.closeSync(fd);\n  }\n}\n// call before recoverLegacyDisk():\n// assertFat32SectorSize(CONSTANTS.IMAGE_PATH);","typeGuard":"function isFat32BootSector(b: Buffer): boolean {\n  const bytesPerSec = b.readUInt16LE(11);\n  const secPerClus = b.readUInt8(13);\n  return (\n    bytesPerSec === 512 &&\n    secPerClus > 0 &&\n    (secPerClus & (secPerClus - 1)) === 0 &&\n    b.readUInt8(510) === 0x55 && b.readUInt8(511) === 0xaa\n  );\n}","tryCatchPattern":"try {\n  const { dir, files } = await recoverLegacyDisk(statePath, outDir);\n} catch (e) {\n  if (e instanceof Error && e.message === \"unexpected sector size\") {\n    // image is not a 512-byte-sector FAT32 volume; abort recovery with guidance\n    throw new Error(\"Disk image must be a 512-byte-sector FAT32 volume; reformat or convert the image first.\", { cause: e });\n  }\n  throw e;\n}","preventionTips":["Format recovery-source images with 512-byte sectors (default `mkfs.vfat`, or `mkfs.vfat -S 512`).","Snap-check the BPB (offset 11 == 512, 0x55AA signature) before shipping or restoring any legacy image.","Never assume partition entry 0x1BE is FAT32 — validate the boot sector before parsing.","Keep a smoke test that runs extractFat32 against a reference 512-byte-sector image in CI.","If adding sector-size flexibility, thread bytesPerSec through all offset math rather than special-casing after the throw."],"tags":["fat32","disk-image","validation","filesystem"],"backgroundTag":"fat32-sector-size-unsupported","analyzedSha":"051065e5ae815203a0a9038e01eefdf3c10519f1","analyzedAt":"2026-08-31T18:49:04.616Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}