libxls: reading the old binary Excel format from C, without exiting on bad input
Read binary Excel files from C/C++
At a glance
- What is it?
- libxls is a C library for reading Excel files in the binary OLE format, and its current generation differs from the 1.4 series in three ways that matter: an API for parsing files already in memory, error returns instead of process exits, and heavy fuzzing. It also ships xls2csv, a command line converter.
- Who is it for?
- Adopt libxls if you need to read legacy XLS files from C, you need to parse a file that is already in memory rather than on disk, or you are hardening a path where malformed spreadsheets arrive from untrusted senders. Do not adopt it for XLSX, since the format it reads is the old binary one, and note the version you pick matters for safety, because 1.5 fixes many vulnerabilities found in 1.4 and earlier.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The old binary format, and why the library exists
libxls is a C library for reading Excel files in the nasty old binary OLE format, plus a command-line tool for converting XLS to CSV, named appropriately enough as xls2csv.
That format description is not editorialising about the format's age, it is the actual scope. The XLS format is a compound document, an OLE container, holding a workbook structure with a stream per worksheet and a record stream per row. The library also ships a CLI tool for converting Excel files to CSV:
./xls2csv /path/to/file.xls
The man page for xls2csv has more details.
For most people the immediate question is why not use a spreadsheet library, and the answer is that the xls format is what a large amount of legacy data actually arrives in, and the C ecosystem needed a way to read it without a heavyweight dependency. The README also notes the format documentation, pointing at the Microsoft Excel File Format and the Compound Document file format, with the documentation provided by the nice folks at OpenOffice.org.
The library should run fine on both little-endian and big-endian systems, though the README asks you to open an issue if it does not.
The licence is one GitHub cannot classify, with the file in the repository root, the primary language is C, the default branch is dev, and the last push was on 2026-10-12. The release history is v1.6.4 from 2026-10-12, v1.6.3 from 2024-12-26 and v1.6.2 from 2021-01-04, so the project is on a 1.6 line with a long gap behind the 1.6.3.
Version 1.5 changed the contract, and that is the point
The README is unusually direct about the state of the library. After several years of neglect, libxls is under new management as of the 1.5.x series, and the README points at the releases page for the latest stable version of libxls 1.5, which fixes many security vulnerabilities found in libxls 1.4 and earlier.
So the version you choose is a safety decision, not just a feature decision. The changes since 1.4 are listed as a short list: hosted on GitHub, a new in-memory parsing API, internals rewritten to return errors instead of exiting, heavy fuzzing with clang's libFuzzer fixing many memory leaks and CVEs, improved compatibility with C++, continuous integration tests on Mac, Linux and Windows, and lots of other small fixes to be found in the commit history.
The two changes that affect your code most are the in-memory API, which the README names as xls_open_buffer, and the error returns. The library no longer exits on malformed input, it returns an error code, which means a malformed spreadsheet uploaded by a user is a value you check rather than a process that vanishes.
That change is easy to underestimate when upgrading. A caller written against 1.4 may have relied on the library terminating the process on a corrupt file, and a caller written against 1.5 has to check every return path, because the process now keeps going.
The fuzzing badge at the top of the README points at the oss-fuzz project, so the library is fuzzed continuously rather than only in a release sweep.
The cell loop is where the real work is
The C API example in the README is the reference for how to walk a workbook, and the shape of it is worth reading closely because the type handling is the part that bites.
The workbook is opened with xls_open_file, taking a path, a charset and a pointer to an error variable, and the result is checked for NULL before anything else happens. The loop then goes over the sheets, and for each sheet it calls xls_getWorkSheet and xls_parseWorkSheet, which is where the sheet content is actually decoded. Then it loops over rows using work_sheet->rows.lastrow, over columns using work_sheet->rows.lastcol, and for each cell it looks at cell->id to decide what the cell actually contains.
The cell id checks are the important part. A blank cell is XLS_RECORD_BLANK and is skipped. A number is XLS_RECORD_NUMBER and the value is in cell->d as a double-precision number. A formula is XLS_RECORD_FORMULA, and then the string in cell->str tells you what the formula evaluates to: the string bool means it is a boolean and you test cell->d greater than zero for true, the string error means the formula is in error, and otherwise cell->str is the valid result of a string formula. If none of those apply and cell->str is not NULL, the cell holds a string value.
So a cell is a tagged union in C, and the caller has to switch on the tag. There is no single accessor that works for every cell, and a program that assumes cell->str is always a string will read garbage on a numeric cell.
Each worksheet is closed with xls_close_WS and the workbook with xls_close_WB, which is the pattern to copy: check errors at open, parse each sheet before reading it, and close both levels.
Testing, fuzzing and the packaging bits around the library
A C parser that reads a binary format written by other software is a parser of untrusted input, and the README says plainly that libxls has been heavily fuzz-tested with clang's libFuzzer, fixing many memory leaks and CVEs. The fuzzing badge at the top links to the oss-fuzz project, so the library is fuzzed continuously rather than only as a release exercise.
That matters more than the word fuzzing usually implies, because the 1.5 rewrite changed the failure behaviour. Internals were rewritten to return errors instead of exiting, which means a malformed file no longer takes the process down. The README also says that if you find a bug, please file it on the GitHub issue tracker.
Continuous integration tests run on Mac, Linux and Windows, and the repository layout shows the pieces of that: a fuzz/ directory holding the harnesses, a test/ directory, and a cplusplus/ directory, which is where the improved C++ compatibility is exercised. There is also a formulas/ directory, which is a Homebrew formula, and a libxls.pc.in file, which is the pkg-config template, so linking against the installed library is a matter of the usual pkg-config flags rather than hardcoded paths.
The AUTHORS file and the man/ directory are the other conventional pieces, and the LICENSE is a custom one that GitHub cannot classify.
Taken together, the shape of the project is a small, careful C library with a maintained toolchain and a strong reason to prefer 1.5 or later over 1.4.
Building it, and the bindings that already exist
Installation is the standard autotools sequence. If you want a stable version, check out the Releases section, which has copies of everything you will find on Sourceforge, and download version 1.5.0 or later. The full instructions are in the INSTALL file, and the short version is:
./configure
make
make installIf you have cloned the git repository rather than downloading a release tarball, you need to run this first:
./autogen.shThat generates all the supporting files, and it assumes autotools is already installed on the system, and also expects the Autoconf Archive to be present.
The language bindings section is a genuine saving. If C is not your cup of tea, you can use libxls in several other languages, and the README lists Haskell through Hackage, R through readxl, Rust through a rust-xls project, Go through a go-xls project, and Crystal through xls.cr. Two things follow from that list. The bindings are separate projects, so their maintenance and their compatibility with a given libxls version are their own business. And the existence of a readxl binding for R tells you there was enough demand for a better interface to warrant one.
The repository layout is conventional for a C library with an autotools build: include/ holds the public headers, src/ the implementation, test/ the test suite, man/ the manual pages, and there is a cplusplus/ directory, which is presumably the C++ compatibility testing, along with fuzz/ for the fuzzing harnesses and formulas/ for a Homebrew formula.
There is also a libxls.pc.in, which is the pkg-config template, so linking against the installed library is a matter of the usual pkg-config flags rather than hardcoded paths.
Editorial conclusion
Adopt libxls if you need to read legacy XLS files from C, you need to parse a file that is already in memory rather than on disk, or you are hardening a path where malformed spreadsheets arrive from untrusted senders. Do not adopt it for XLSX, since the format it reads is the old binary one, and note the version you pick matters for safety, because 1.5 fixes many vulnerabilities found in 1.4 and earlier. Before you integrate, read the cell type handling in the example, since a cell can be blank, a number, a formula or a string and the library gives you different members for each.
Frequently asked questions
What file formats can libxls read?
It reads Excel files in the old binary OLE format, the nasty old binary one, not XLSX. The README points at the Microsoft Excel File Format and the Compound Document file format documentation, and the library also ships xls2csv, a command line tool for converting the files to CSV.
How do I parse an XLS file that is already in memory with libxls?
Version 1.5 added an in-memory parsing API, with the entry point named xls_open_buffer. Before 1.5 there was no such API, so if you are on 1.4 or earlier you have to write the buffer to a file first.
What changed in libxls 1.5 compared to 1.4?
It is hosted on GitHub, it added the in-memory parsing API, the internals were rewritten to return errors instead of exiting, it was heavily fuzz-tested with clang's libFuzzer fixing many memory leaks and CVEs, C++ compatibility was improved, and there is continuous integration on Mac, Linux and Windows. The 1.5 release also fixes many security vulnerabilities found in 1.4 and earlier.
How do I build libxls from source?
For a release tarball, run ./configure, make and make install. If you cloned the git repository, run ./autogen.sh first to generate the supporting files, which assumes autotools and the Autoconf Archive are installed.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/libxls-libxls)Was this useful?