Add MS-OLEPS SummaryInformation metadata read/write - #887
Merged
Conversation
Mearman
marked this pull request as ready for review
September 3, 2026 16:49
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Mearman
force-pushed
the
feat/summary-information-metadata
branch
from
September 3, 2026 18:19
43a510b to
7862a40
Compare
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
force-pushed
the
feat/summary-information-metadata
branch
from
September 3, 2026 18:45
7862a40 to
ab79ed5
Compare
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
force-pushed
the
feat/summary-information-metadata
branch
from
September 3, 2026 18:56
ab79ed5 to
5444726
Compare
This was referenced Sep 3, 2026
Contributor
|
🎉 This PR is included in version 1.1.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Contributor
|
🎉 This PR is included in version 1.1.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Contributor
|
🎉 This PR is included in version 2.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.doc-codec,xls-codec, andppt-codec: each package's reader now maps a"\x05SummaryInformation"stream (when present) ontoLayoutMetadata, 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 insidearchive-codecrather 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.tsgeneric container parsing +cfb/ole-package.tsa specific stream format built on top) is exactly the shape this needed:oleps/wire.ts+read.ts+write.tsare the generic, zero-document-knowledge [MS-OLEPS] codec, andoleps/summary-information.tsis the SummaryInformation-specific mapping on top of it, mirroringcfb/ole-package.ts's own layering overcfb/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, neverVT_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 decodesVT_LPSTRunderCP_WINUNICODEor windows-1252, since real Office-authored streams almost always use ANSI strings.LayoutMetadata's owncomments/lastPrintedIsogap (SummaryInformation can carry them,LayoutMetadatahas no field for either) andcreator/producer/languagegap (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'sreadWorkbookStreamis replaced byreadWorkbookStreams, 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.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 viaarchive-codec's own writer.archive-codec,doc-codec,xls-codec, andppt-codec, plus a full-workspacepnpm exec turbo run _lint _typechecksanity pass and a full-workspacepnpm typecheck && pnpm testvia the pre-push hook on every push.Progresses #815, #816, #817.