Parsing 1990s Word documents in Go by running C inside WebAssembly

Long after Word moved to the XML-based .docx, organizations still sit on archives of the old binary .doc format, and sooner or later something modern needs to read them. Most current parsing libraries understandably focus on .docx; the old format, a container of OLE compound-document streams, is the part nobody wants to touch.

I needed exactly that in a Go service: extract text and metadata from legacy .doc files. The result is go-catdoc, which runs a 1990s C program inside the Go process, through WebAssembly.

The usual options for using C from Go

There’s a battle-tested tool for this already: catdoc, a small C program that has been reading .doc files since the nineties. The question is how to use it from Go.

Shelling out to the catdoc binary means your deployment needs catdoc installed, in the right version, on every target machine and in every container image. The library stops being self-contained. Binding it with cgo works, but costs you painless cross-compilation and adds a C toolchain to every build environment, which is a lot of infrastructure for a small parsing dependency. And reimplementing the format in pure Go is off the table for me: the OLE compound document format plus the Word binary format is an enormous, quirk-ridden spec, and a rewrite would trail catdoc’s decades of accumulated edge cases for years.

Instead, I compiled catdoc itself to WebAssembly with Emscripten, embedded the resulting binary in the Go package with go:embed, and executed it with wazero, a WebAssembly runtime written in pure Go.

The whole compiled module is about 90 KB. The Go side embeds it, compiles it once, and keeps the compiled module in a cache so repeated calls don’t pay the startup cost again. Because wazero itself has no cgo anywhere, the package remains a normal Go dependency: go get, cross-compile to any platform Go supports, done. The C code runs sandboxed inside the process, with no access to anything you don’t explicitly hand it.

Using it looks like any other Go library:

f, _ := os.Open("report.doc")
text, err := gocatdoc.GetTextFromFile(f)

Faking a filesystem

catdoc is a command-line program. It expects to open a file by path, and it loads its character-set tables from files too. WebAssembly modules under WASI see the filesystem you mount for them, so go-catdoc hands the module a fake one: a minimal fs.FS implementation that wraps the caller’s io.ReadSeeker and serves it as a file mounted at a fixed path, with the charset tables embedded in the package and mounted alongside. catdoc opens what it thinks are files on disk and actually reads from whatever reader you passed in, which never has to touch disk at all.

Extracting the document metadata

Text extraction worked with stock catdoc, but I also needed document metadata: title, author, last author, subject, keywords, comments. That lives in a separate OLE stream called SummaryInformation, which catdoc didn’t expose. So the vendored catdoc source is lightly modified to read it, and to pull annotation authors out as well.

To make sense of the binary layout while doing that, I wrote an ImHex pattern file that parses the SummaryInformation object visually. It ships in the repository, so if you ever need to poke at .doc internals yourself, that’s a decent starting point.

When this pattern is the right call

Wrapping proven C in WebAssembly instead of rewriting it is something I’d reach for again, and not just for document formats. It fits when the existing C code is genuinely trusted, the format or protocol is too gnarly to reimplement economically, and you want a self-contained library rather than a deployment dependency. You get memory isolation as a bonus: a parser bug in the C corrupts its own sandbox and your process survives.

It’s the same approach I take to legacy modernization work in general: keep the code that has proven itself, and put a boundary you can test around it.

Working on something similar?

Tell me what you're trying to build. I'll let you know honestly whether I'm a good fit.

Get in touch
Start a conversation