Skip to content

Fix is_encrypted() on an XLS stream ending in a partial record header - #106

Merged
nolze merged 2 commits into
nolze:masterfrom
usmanmateen:fix/has-record-short-header
Oct 11, 2026
Merged

nolze merged 2 commits into
nolze:masterfrom
usmanmateen:fix/has-record-short-header

Conversation

@usmanmateen

Copy link
Copy Markdown
Contributor

Fixes #83.

Xls97File.is_encrypted() raised struct.error: unpack requires a buffer of 4 bytes for many XLS files written by xlwt. xlwt pads the Workbook stream with zeros to a multiple of 4096 bytes. has_record() reads that padding as empty 4-byte records, and when 1–3 bytes are left, unpack("<HH", h) fails, because it only stopped when read(4) returned nothing. Whether a file is affected depends on its size: of six xlwt workbooks with 1, 2, 5, 10, 50 and 200 rows, four raised.

has_record() now treats a partial header (fewer than 4 bytes) as the end of the stream, as you suggested in #83. This doesn't miss an encrypted file: in rc4cryptoapi_password.xls and xor_password_123456789012345.xls, FilePass is the record directly after BOF, so it is found before any padding. I left skip_to() and iter_record() as they are. skip_to() is only called for a record that is known to be there, and iter_record() feeds decryption, where the trailing bytes should not be dropped silently.

tests/test_xls_padding.py checks is_encrypted() on tests/inputs/plain_xlwt_padding.xls, a 3-row workbook written by xlwt 1.3.0. It fails without the change and passes with it. The other tests and the xls97 doctests pass. (test_cli fails on my checkout with and without the change, because test_cli.sh got CRLF line endings on Windows.)

has_record() read 4-byte record headers until read() returned nothing,
so a Workbook stream ending in 1-3 bytes of padding raised
struct.error. xlwt pads the stream with zeros to 4096 bytes, so this
happens for many of its files. Treat a partial header as the end of the
stream.

Fixes nolze#83
@nolze

nolze commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Thank you for the fix! Thanks to your sample, I found that the Globals Substream has an EOF record before the trailing padding. Since the spec places FilePass in that substream, stopping the search at its EOF resolves the issue without accepting partial record headers. I’ve updated the fix accordingly.

@nolze
nolze merged commit 50be564 into nolze:master Oct 11, 2026
17 checks passed
@codecov

codecov Bot commented Oct 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.52%. Comparing base (0f320cb) to head (4fc877f).
⚠️ Report is 29 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #106      +/-   ##
==========================================
- Coverage   92.09%   91.52%   -0.57%     
==========================================
  Files          15       15              
  Lines        1847     1841       -6     
==========================================
- Hits         1701     1685      -16     
- Misses        146      156      +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

struct.error: unpack requires a buffer of 4 bytes

2 participants