Skip to content

Add MS-OLEPS SummaryInformation metadata read/write - #887

Merged
Mearman merged 12 commits into
mainfrom
feat/summary-information-metadata
Sep 3, 2026
Merged

Add MS-OLEPS SummaryInformation metadata read/write#887
Mearman merged 12 commits into
mainfrom
feat/summary-information-metadata

Conversation

@Mearman

@Mearman Mearman commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a generic reader/writer for the MS-OLEPS Property Set Stream format to archive-codec (packages/archive-codec/src/oleps/), plus a SummaryInformation-specific layer mapping the seven fields a legacy binary Office document's title/author/dates live in onto named property identifiers.
  • Wires it into doc-codec, xls-codec, and ppt-codec: each package's reader now maps a "\x05SummaryInformation" stream (when present) onto LayoutMetadata, and each writer writes one back whenever the input's metadata carries anything representable.

Design decision: archive-codec vs. a new foundation package

oleps/ was added inside archive-codec rather than as a new sibling package. archive-codec's own README already states its charter as "the archive and container utility package for the documents.js family" and its precedent (cfb/read.ts generic container parsing + cfb/ole-package.ts a specific stream format built on top) is exactly the shape this needed: oleps/wire.ts+read.ts+write.ts are the generic, zero-document-knowledge [MS-OLEPS] codec, and oleps/summary-information.ts is the SummaryInformation-specific mapping on top of it, mirroring cfb/ole-package.ts's own layering over cfb/read.ts. A Property Set Stream only ever appears inside an OLE compound file, so it belongs with the CFB support already here rather than in a new package with no other reason to exist.

Scope

Only the fixed SummaryInformation property set (title/subject/author/keywords/comments/created/last-saved/last-printed) is read or written. DocumentSummaryInformation's extended and user-defined property sets (a separate two-property-set stream) are explicitly out of scope, tracked in each touched package's own README.

The generic writer only emits VT_LPWSTR (Unicode) strings, never VT_LPSTR (ANSI/codepage-dependent) — writing an arbitrary ANSI codepage's bytes would need a full codepage table this package doesn't have, and Unicode strings sidestep the question entirely. The reader still decodes VT_LPSTR under CP_WINUNICODE or windows-1252, since real Office-authored streams almost always use ANSI strings.

LayoutMetadata's own comments/lastPrintedIso gap (SummaryInformation can carry them, LayoutMetadata has no field for either) and creator/producer/language gap (the reverse) are documented per-package rather than papered over; see each README's own "Metadata" section for the exact field mapping and its two permanent gaps.

xls-codec's readWorkbookStream is replaced by readWorkbookStreams, returning both the Workbook stream and the optional metadata stream from one compound-file parse rather than two; its only caller (content.ts) is updated accordingly — a genuine breaking rename within the package, not a compat shim.

Verification

  • oleps's reader is tested primarily against [MS-OLEPS]'s own worked "SummaryInformation Property Set" example, transcribed byte-for-byte from the spec, independently re-deriving the three FILETIME timestamps from the documented formula rather than trusting this package's own conversion.
  • Each of doc-codec/xls-codec/ppt-codec's metadata read/write is verified by round-tripping through its own reader, plus a dedicated read-side test injecting a real SummaryInformation stream via archive-codec's own writer.
  • Full check suite (lint/typecheck/test/test:workers/test:smoke) green for archive-codec, doc-codec, xls-codec, and ppt-codec, plus a full-workspace pnpm exec turbo run _lint _typecheck sanity pass and a full-workspace pnpm typecheck && pnpm test via the pre-push hook on every push.

Progresses #815, #816, #817.

@Mearman
Mearman marked this pull request as ready for review September 3, 2026 16:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-03T16:58:03.337466Z 43a510b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman force-pushed the feat/summary-information-metadata branch from 43a510b to 7862a40 Compare September 3, 2026 18:19
@Mearman

Mearman commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Retriggering CI -- the main ci.yml pull_request workflow never fired after the last force-push (only the separate CodeQL Analyze workflow ran).

@Mearman Mearman closed this Sep 3, 2026
@Mearman Mearman reopened this Sep 3, 2026
@Mearman
Mearman force-pushed the feat/summary-information-metadata branch from 7862a40 to ab79ed5 Compare September 3, 2026 18:45
Adds a generic reader and writer for the MS-OLEPS Property Set Stream
format (stream header, PropertySet dictionary, and VT_I2/VT_I4/VT_LPSTR/
VT_LPWSTR/VT_FILETIME typed values), plus a SummaryInformation-specific
layer mapping the seven fields a legacy binary Office document's title,
author, and dates live in (title/subject/author/keywords/comments/
created/last-saved/last-printed) onto named PIDs.

The generic reader accepts VT_LPSTR under either CP_WINUNICODE or
windows-1252 (the two codepages real producers actually use), decoding
each string up to its first null character per MS-OLEPS's own tolerance
for embedded/trailing nulls. The writer only emits VT_LPWSTR strings,
sidestepping the ANSI codepage-table question entirely since Unicode
strings are codepage-independent; VT_LPSTR write is refused with a named
error. NumPropertySets is restricted to 1 and PID 0 (the Dictionary
property) is rejected, since neither ever appears in a real
SummaryInformation stream and DocumentSummaryInformation's two-property-
set, named-property spelling is an explicit, separately-scoped
remainder.

The primary reader test transcribes MS-OLEPS's own worked
SummaryInformation Property Set example byte for byte, independently
re-deriving the three FILETIME timestamps from the documented
100-nanosecond-since-1601 formula rather than trusting this package's
own conversion.
readDocContent now maps a "\x05SummaryInformation" stream, when the
compound file carries one, onto LayoutMetadata's title/subject/author/
keywords/createdIso/modifiedIso through archive-codec's oleps support;
a document with no such stream still reads back {}, exactly as before.
writeDocContent writes the stream back whenever the input's metadata
carries anything SummaryInformation can hold, leaving it out entirely
for metadata that is empty or carries only fields (creator/producer/
language) the stream has no room for, so a caller reading either back
sees the identical {}.

The field mapping lives in a new src/metadata.ts: comments and
last-printed have no LayoutMetadata destination, and creator/producer/
language have no SummaryInformation source, so each direction only
carries the six fields both sides can actually represent.
…tion

readXlsContent now maps a "\x05SummaryInformation" stream, when the
compound file carries one, onto LayoutMetadata's title/subject/author/
keywords/createdIso/modifiedIso through archive-codec's oleps support;
a workbook with no such stream still reads back {}, exactly as before.
writeXlsContent writes the stream back whenever the input's metadata
carries anything SummaryInformation can hold, leaving it out entirely
for metadata that is empty or carries only fields (creator/producer/
language) the stream has no room for.

readWorkbookStream is replaced by readWorkbookStreams, returning both
the Workbook stream and the optional metadata stream from one compound-
file parse rather than two; its only caller (content.ts) is updated
accordingly.

The field mapping lives in a new src/metadata.ts, identical in shape to
doc-codec's: comments and last-printed have no LayoutMetadata
destination, and creator/producer/language have no SummaryInformation
source, so each direction only carries the six fields both sides can
actually represent.

BREAKING CHANGE: readWorkbookStream is renamed to readWorkbookStreams
and now returns { workbook, metadata } instead of a bare workbook byte
array. A caller importing readWorkbookStream must switch to
readWorkbookStreams and destructure the workbook field.
readPptContent now maps a "\x05SummaryInformation" stream, when the
compound file carries one, onto LayoutMetadata's title/subject/author/
keywords/createdIso/modifiedIso through archive-codec's oleps support;
a presentation with no such stream still reads back {}, exactly as
before. writePptContent writes the stream back whenever the input's
metadata carries anything SummaryInformation can hold, leaving it out
entirely for metadata that is empty or carries only fields (creator/
producer/language) the stream has no room for.

readPptStreams/writePptStreams, the record-level split one layer below
readPptContent/writePptContent, are left untouched: they take or return
only the two required [MS-PPT] streams and have no compound file to
look a third stream up in, so the container-level metadata lookup lives
one layer up, where readPptContent already has the full stream list
from its own readCompoundFile call.

The field mapping lives in a new src/metadata.ts, identical in shape to
doc-codec's and xls-codec's: comments and last-printed have no
LayoutMetadata destination, and creator/producer/language have no
SummaryInformation source, so each direction only carries the six
fields both sides can actually represent.
…borting the whole read

readPropertySetStream threw PropertySetFormatError for any PropertyType
outside VT_I2/VT_I4/VT_LPSTR/VT_LPWSTR/VT_FILETIME, and for a VT_LPSTR
under any CodePage other than CP_WINUNICODE/windows-1252. Both are
routine in real SummaryInformation streams: PIDSI_THUMBNAIL (PID 0x11)
is VT_CF, written whenever "save preview picture" is on, and a
non-Western document's CodePage is routinely something other than
1200/1252. Since doc-codec/xls-codec/ppt-codec call readSummaryInformation
with no guard, either case turned an unrelated metadata field into a
total read failure for the whole document.

An unsupported PropertyType or CodePage is now skipped -- the property
is simply absent from the returned map, matching how a PID this reader
doesn't project (PIDSI_TEMPLATE and friends) was already handled.
Genuine structural nonconformance (a bad ByteOrder, a truncated
stream, a Dictionary property, non-zero TypedPropertyValue padding, a
CodePage property of the wrong type) still throws.

Also corrects the module comment's claim that the five decoded
PropertyType values cover every property a real SummaryInformation
stream carries -- PIDSI_THUMBNAIL/VT_CF is a real counterexample.
…FILETIME conversion

layoutMetadataToSummaryInformation passed LayoutMetadata.createdIso/
modifiedIso straight through to archive-codec's
writeSummaryInformationStream, which converts each into a FILETIME via
`new Date(iso)`. A malformed string produces an Invalid Date, whose
getTime() is NaN, and BigInt(NaN) then throws an opaque RangeError with
no indication which field or package caused it.

Both fields are now validated as real dates before crossing into
archive-codec's own shape, throwing a DocFormatError that names the
offending field instead.
…FILETIME conversion

layoutMetadataToSummaryInformation passed LayoutMetadata.createdIso/
modifiedIso straight through to archive-codec's
writeSummaryInformationStream, which converts each into a FILETIME via
`new Date(iso)`. A malformed string produces an Invalid Date, whose
getTime() is NaN, and BigInt(NaN) then throws an opaque RangeError with
no indication which field or package caused it.

Both fields are now validated as real dates before crossing into
archive-codec's own shape, throwing a BiffWriteError that names the
offending field instead.
…FILETIME conversion

layoutMetadataToSummaryInformation passed LayoutMetadata.createdIso/
modifiedIso straight through to archive-codec's
writeSummaryInformationStream, which converts each into a FILETIME via
`new Date(iso)`. A malformed string produces an Invalid Date, whose
getTime() is NaN, and BigInt(NaN) then throws an opaque RangeError with
no indication which field or package caused it.

Both fields are now validated as real dates before crossing into
archive-codec's own shape, throwing a PptUnsupportedContentError that
names the offending field instead -- the same write-side error class
this package's own convention reserves for content outside this
writer's scope.
…perties mapping

doc-codec, xls-codec, and ppt-codec each carried a byte-identical
src/metadata.ts mapping SummaryInformationProperties to and from
document-schema.js's LayoutMetadata, differing only in a few words of
comment. The mapping itself is format-agnostic -- nothing about it is
specific to .doc, .xls, or .ppt -- so it now lives once, in a new
oleps/layout-metadata module alongside the property-set codec it sits
on top of, exported from the package barrel.

Depends on document-schema.js for the LayoutMetadata type: a
foundation-to-foundation dependency already established by
document-outline.js, and confirmed not to break Worker-isomorphism
(test:workers still passes).
…pping

summaryInformationToLayoutMetadata and hasSummaryInformationFields were
a byte-identical copy of xls-codec's and ppt-codec's own -- format-
agnostic mapping logic that had nothing to do with .doc specifically.
Both now come directly from archive-codec; src/metadata.ts keeps only
what is genuinely this package's own: layoutMetadataToSummaryInformation
wrapped with createdIso/modifiedIso date validation, reporting a
malformed date as a DocFormatError rather than delegating straight
through.
…pping

summaryInformationToLayoutMetadata and hasSummaryInformationFields were
a byte-identical copy of doc-codec's and ppt-codec's own -- format-
agnostic mapping logic that had nothing to do with .xls specifically.
Both now come directly from archive-codec; src/metadata.ts keeps only
what is genuinely this package's own: layoutMetadataToSummaryInformation
wrapped with createdIso/modifiedIso date validation, reporting a
malformed date as a BiffWriteError rather than delegating straight
through.
…pping

summaryInformationToLayoutMetadata and hasSummaryInformationFields were
a byte-identical copy of doc-codec's and xls-codec's own -- format-
agnostic mapping logic that had nothing to do with .ppt specifically.
Both now come directly from archive-codec; src/metadata.ts keeps only
what is genuinely this package's own: layoutMetadataToSummaryInformation
wrapped with createdIso/modifiedIso date validation, reporting a
malformed date as a PptUnsupportedContentError rather than delegating
straight through.
@Mearman
Mearman force-pushed the feat/summary-information-metadata branch from ab79ed5 to 5444726 Compare September 3, 2026 18:56
@Mearman
Mearman merged commit 7d0c81d into main Sep 3, 2026
17 checks passed
@Mearman
Mearman deleted the feat/summary-information-metadata branch September 3, 2026 19:03
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant