DEV Community

Jaime Moreno
Jaime Moreno

Posted on AI-assisted

I built a graveyard for dead file formats. Here is how .dbf and .mdb files open in the browser

Somewhere in an office right now, a 2009 program is still sending invoices. Nobody has the install disk, the developer retired, and the data lives in a .dbf or an .mdb that nobody remembers how to open.

I work on legacy migrations, so I see this constantly. I wanted a way to say "drop the file here and look at your data" without asking anyone to install anything or upload sensitive company data to a server. So I built a small set of free tools, the File Format Graveyard, and here is how the interesting parts work.

The one rule: the file never leaves the browser

Everything runs client-side. The file goes through file.arrayBuffer() and gets parsed in JavaScript. No upload, no backend, no privacy question, and hosting is a static site. That constraint shaped every decision below.

Reading a .dbf in a few lines

A .dbf (dBase, FoxPro, Clipper) is simpler than it looks. A 32-byte header, then 32-byte field descriptors, then fixed-width records:

const dv = new DataView(buffer);
const records      = dv.getUint32(4, true);  // number of records
const headerLength = dv.getUint16(8, true);
const recordLength = dv.getUint16(10, true);

// Field descriptors: 32 bytes each from offset 32, ending at a 0x0D byte
for (let off = 32; u8[off] !== 0x0d; off += 32) {
  const name   = ascii(u8, off, 11);
  const type   = String.fromCharCode(u8[off + 11]); // C, N, D, L, M...
  const length = u8[off + 16];
}

// Every record starts with a flag byte: 0x2A ('*') means "deleted"
const isDeleted = (i) => u8[headerLength + i * recordLength] === 0x2a;
Enter fullscreen mode Exit fullscreen mode

The format has a few traps, though.

1. Accents are a code page problem. Old DBF files do not use UTF-8. The header declares a code page in byte 29, and TextDecoder does not support the DOS ones (850, 437). The fix is a 128-character table for the upper half of the byte range:

const CP850 = 'ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒáíóúñѪ...'; // 128 chars
const decode = (u8, start, end) => {
  let s = '';
  for (let i = start; i < end; i++) s += u8[i] < 128 ? String.fromCharCode(u8[i]) : CP850[u8[i] - 128];
  return s;
};
Enter fullscreen mode Exit fullscreen mode

Byte 0x82 is é in DOS 850, but a low quotation mark in Windows-1252. Decode "José María" with the wrong one and you get "Jos‚ Mar¡a". So the viewer detects the code page from the header and lets you change it live.

2. Visual FoxPro adds binary types. Integers are plain int32, currency is an int64 divided by 10,000, and datetimes are a Julian day number plus milliseconds since midnight.

3. Deleted records are still in the file, just flagged. The viewer hides them by default.

4. The header can lie. If it declares more records than the file actually holds (a truncated copy, a hand edit), reading blindly gives garbage. I clamp the count with floor((fileSize - headerLength) / recordLength).

5. Memos live in another file. Long text fields point into a .fpt or .dbt file. I do not read those, so they show as (memo) with a warning instead of pretending the data is complete.

Access without Access

For .mdb and .accdb I did not write a parser. mdb-reader (MIT) reads Jet 3/4 and ACE files, including password-protected ones. Two things were needed in the browser:

// Loaded only when someone drops a file, so the rest of the site stays light
const { Buffer } = await import('buffer');
globalThis.Buffer ??= Buffer;
// A dependency written for Node asked for `process`; a tiny shim is enough
globalThis.process ??= { env: {}, browser: true, nextTick: (fn, ...a) => queueMicrotask(() => fn(...a)) };

const { default: MDBReader } = await import('mdb-reader');
const reader = new MDBReader(Buffer.from(arrayBuffer));
reader.getTableNames();
reader.getTable('Customers').getData({ rowOffset: 0, rowLimit: 100 });
Enter fullscreen mode Exit fullscreen mode

The process is not defined error only showed up in the production build, not during my first tests, which is a good reminder to test the bundle you actually ship.

Getting data into Excel

CSV export sounds trivial until Excel opens it. Two details matter: a UTF-8 BOM, so accents survive, and a semicolon separator for locales that use a decimal comma. The export builds a Blob in chunks and yields to the event loop every few thousand rows, so a big table does not freeze the tab. Search works the same way, scanning in chunks of 20,000 rows.

Testing without anyone's real data

I cannot commit customer files, so the DBF tests build synthetic files byte by byte (a dBase III file with deleted records and DOS 850 accents, a Visual FoxPro file with binary types, a truncated file with a lying header). For Access I used the sample databases from the library's own repository. One test failed at first because I had miscalculated the Julian day in the test, not the parser. Writing the expected value by hand is humbling.

Limits

It is read-only. It does not read memo files or indexes, and files over 300 MB are better served by a desktop tool.

Try it, and tell me what is next

Which dead format should I tackle next? Paradox? Lotus 1-2-3? Works? If you have a file that nothing will open, tell me what it is.

Top comments (0)