Releases: dkulyk/openxml-package
Releases · dkulyk/openxml-package
Release list
v0.12.0
Added
saveTo()writes a package to an open stream, which need not be seekable, so
a download no longer needs a temporary file and its first bytes leave
immediately. Sending a package with a 64 MiB part through a pipe takes about
21 ms against about 55 ms for writing a temporary file and copying it out, and
its first byte arrives at once rather than after about 30 ms;composer benchmark-stream-savereports both. An entry whose size is unknown in advance
carries a trailing descriptor when the destination cannot seek; a seekable
destination still patches the local header, so saving to a file produces the
same archive it did before.PackageInterfacedeclaressaveTo()alongside
save()andsaveAs(), so a caller typed against the interface reaches it.
Fixed
- A DTD in package XML or in an
EncryptionInfostream is refused whatever the
document's encoding. The check scanned the raw bytes for<!DOCTYPE, which a
UTF-16 document hides behind a high byte after every character, so such a
package was accepted and its internal entities substituted into attribute and
element values. Known XML byte encodings are checked before parsing, and the
parsed document is still asked for its doctype as defence in depth. This avoids
spending CPU and memory on internal entity substitution before rejection. Reading
external entities and expanding entities beyond libxml's own amplification
limit were already refused, so no file disclosure or expansion attack was
reachable through the original encoding gap. Package XML andEncryptionInfo
written in EBCDIC are refused outright: libxml decodes that encoding, but it
is neither ASCII-compatible nor zero-padded, so no byte signature can see a
DTD through it, and no OPC package uses it. Rejecting an 8.6 MiB document of
three million entity references costs 0.1 ms and no measurable memory, against
0.4 seconds and 300 MiB spent expanding it before the parsed document was
asked for its doctype.
v0.11.0
Added
- Checksums come from zlib instead of PHP. A deflated entry is handed to zlib as
a gzip stream, so its checksum and length are verified while it is decoded
rather than in a second pass. Decoding 16 MiB of incompressible data goes from
33 ms to 3.9 ms, and reading a package of 2000 small parts is about a tenth
faster. A part stream hands decoded chunks over without
copying them, so reading 16 MiB throughopenStream()costs 2.9 ms against
3.4 ms throughZipArchive::getStream(). - A part copied from one open package to another moves its compressed bytes as
they are, when nothing has read from its stream and the destination wants the
compression the source used. Moving a 16 MiB part goes from 99 ms to 0.4 ms.
No API changes; the source file must not change before the destination is
saved, and one that does raisesConcurrentModificationException.
Fixed
- An entry whose directory understates how much it holds is refused after about
a megabyte rather than after the whole expansion. - An archive of exactly 65535 entries is written with a ZIP64 record; without one
this library refused to read back what it had just written.
Removed
- Breaking:
ext-zipis no longer required. The runtime now needs PHP 8.1,
ext-domandext-zlib; the public API is unchanged, and CI runs an
end-to-end check on a build with the extension disabled.
Changed
- Validation, repair, inbound-reference lookup, part moves/removal, and signature removal
no longer create and retain empty relationship collections for parts without
a relationship part. Explicitly requested collections remain live and writable. - The ZIP central directory is read by the library instead of by ext-zip.
Opening a package and detecting an Office file walk it lazily; on a 4000-entry
package a container open goes from 8.6 ms to 4.9 ms and detection from 1.12 ms
to 0.05 ms. - Saving writes the archive directly instead of copying the source file and
letting ext-zip rewrite it; unchanged entries that sit side by side move in one
pass. Replacing one part of a 200-part, 12.6 MB package goes from 16.0 ms to
8.8 ms, and peak memory during a save from 30 MB to 4 MB.[Content_Types].xml
is always the first entry; an entry of 4 GiB or more is refused. - Entry contents are decompressed by the library. A part is decoded in chunks,
so reading one as a stream no longer materialises it, and it is accepted only
when its size, its CRC-32 and the number of compressed bytes consumed all agree
with its directory record. Opening a package and opening a part stream both got
faster.
v0.10.0
Added
OpenXmlPackage::removeParts()andremovePartsAndRelationships()remove a
list of parts with one relationship scan instead of one scan per part. Names
and inbound references are checked before removal; cascading results preserve
input order and report each equivalent part name once.removeParts()is
blocked only by a reference from a part the batch keeps, so it accepts what the
loop of single removals it replaces accepts, whatever the order of the names.
PackageInterfaceis unchanged.
Changed
- Breaking.
addPartFromPath(),writePartFromPath()and
setContentsFromPath()no longer copy the file into package-owned storage. The
file is read when the part is read or the package is saved, so the caller owns
it until then and must not move, replace, or delete it. The file's identity and
timestamps are recorded when the part is added and checked on every read, so a
file changed behind the package's back raisesConcurrentModificationException
instead of being silently packaged. Pass a stream to keep the old snapshot
behaviour.getPartReadablePath()returns that file for such a part rather than
materializing a copy of it; every other staged part is still materialized, since
only a file the caller owns outlives the staging. Adding a 64 MiB file and saving
drops from about 87 ms to 66 ms, forty 2 MiB images from about 120 ms to 80 ms,
andgetPartReadablePath()on a 64 MiB part from 18 ms to nothing, because the
bytes are no longer read to stage them and written again to save them. - Opening a stream for file-backed staged part contents now uses an independent
read-only handle to the existing temporary file instead of copying the payload.
Open readers retain their snapshot across part replacement, removal, moves,
saves, and package release; the file is cleaned up after its last owner releases it.
v0.9.0
Added
ContentCompression::store()registers content types whose payload is already
a compressed stream, so a codec this library does not yet know about is
declared once at start-up instead of being special-cased at every call site.addPart(),addPartFromStream(),addPartFromPath(),writePart(),
writePartFromStream(),writePartFromPath()and thesetContents*()methods
of a part take a$compressargument that overrides what the content type
implies for that one part.null, the default, keeps the existing behaviour.
PackageInterfaceandPartInterfacedeclare the argument, so an outside
implementation of either has to accept it.
Changed
[Content_Types].xmland relationship parts are serialized as strings rather
than through DOM. The output is byte for byte what it was, escaping included.- Reads go straight to their source: an unchanged entry is read from the archive
rather than through a stream and its wrapper objects, and a staged part is read
where it already is instead of being copied first. - Relationship collections are held for the package's lifetime instead of only
while something else keeps them alive. Every whole-package operation walks all
relationships, and each walk re-parsed every.relspart. The package keeps
referring to itself weakly from the collections, so nothing holds a cycle and
the source archive is still released with the package. - Parts whose content type is already a compressed stream — JPEG, PNG, GIF,
WebP, HEIC, HEIF, AVIF, JP2, MP3, MP4, Ogg and WebM audio, anyvideo/*, ZIP,
gzip, and embedded OPC or OpenDocument documents — are stored in the package
instead of being deflated a second time. The types are matched exactly, not by
family: an embedded deck is…presentationml.presentationand a slide inside
the package is…presentationml.slide+xml, and only the first is a ZIP.
Everything else, including SVG, BMP, EMF, WMF, VML, OLE objects and any
unrecognised type, is still deflated.
Fixed
ContentTypes::toXml()no longer sorts the collection's own arrays as a side
effect of serializing it.- A package containing an entry that the content types do not cover no longer
becomes unusable.getParts()skipped nothing and threw, which also stopped
getInboundRelationships(),removePart(),removePartAndRelationships()
andmovePart(), so the package could be opened and validated but never
repaired.getParts()now skips such an entry,validate()still reports it,
andsetDefaultContentType()makes it a part. PartName::normalize()no longer clears its whole validation cache when it
fills up. A package with more part names than the cache bound made every pass
over it miss on every name; the bound is now above the name count of a large
package and eviction drops the older half.- ZIP entries are read through a bound set by the size their directory declares.
Package limits were checked once against that directory and never again, so an
entry declaring a small size and inflating to a large one delivered every byte
throughgetContents(),openStream(), andgetLocalPath(). Such an entry is
now rejected with aPackageLimitExceptionwhile it is read. PartInterface::getContentType()reports the content type registered now
rather than the one registered when the handle was made, so a part obtained
beforesetDefaultContentType()no longer reports a stale type. The package
exposes the same lookup asgetPartContentType().
v0.8.1
Added
getMainDocumentPart()returns the part targeted by the package-levelofficeDocumentrelationship, andopen()accepts anexpectingcontent type, or list of them, that the main document part must match. The library does not enumerate document kinds; callers pass the content types they support and getUnsupportedFileFormatExceptionfor anything else. The check reads the package relationships directly and runs before part names are validated and indexed, so rejecting a 1000-part package of the wrong kind costs about 2.5 ms instead of the 3.1 ms a full open takes.
Fixed
- A ZIP archive without
[Content_Types].xml, such as an OpenDocument file or a plain archive, is no longer detected as an OPC package.detect()reports it asUnknownandopen()throwsUnsupportedFileFormatExceptioninstead of the genericOpenXmlExceptionit raised after opening the archive.
See the full changelog.
v0.8.0
Added
setDefaultContentType()declares aDefaultcontent type by extension, so consumers can cover media parts with one declaration instead of anOverrideper part.
Changed
- Breaking: the source package is no longer hashed with SHA-256 when it is opened, and the written output is no longer hashed before it replaces the source. Concurrent-modification checks before lazy reads and saves now rely only on file identity, size, and second-resolution timestamps, which already guarded every read. Lazily opening a 16 MiB package dropped from about 44 ms to under 1 ms, and its copy-through save from about 66 ms to about 23 ms.
- New packages declare
Default Extension="xml"asapplication/xmlalongsiderels, and adding or moving a part whose content type already matches the default for its extension no longer writes anOverride. - Package byte and entry limits are checked against running totals instead of rescanning every entry on each write, so adding 4000 parts dropped from about 360 ms to about 6 ms and part registration is linear again.
- Part-name validation remembers names it already accepted, so building, validating, and saving a 193-part package dropped from about 10.6 ms to about 6.4 ms. The cache is bounded and is reset after 4096 names.
- Saving verifies the written archive by reading back only its structure and
[Content_Types].xml, and reopens the saved package on its first part access instead of immediately, so a package that is saved and released never reads its output back. Validating and saving a 193-part package dropped from about 6.5 ms to about 5.4 ms.
See the full changelog.
v0.7.0
Added
getPart()returns relationship parts, so/_rels/.relsand part relationship files can be read throughPartInterfaceas well as the raw part access methods.- Relationships can be added before their source part exists.
validate()and saving report relationship parts whose source part never arrived. - Benchmark coverage for stream opening, small part reads, part registration, and bulk relationship additions.
Changed
- Breaking: raw part-writing methods and
PartInterface::setContents*()reject relationship parts. Change relationships throughgetRelationships(),addRelationship(), andremoveRelationship()so the in-memory collection and its XML stay synchronized. - Breaking:
getRelationships()andaddRelationship()no longer throwPartNotFoundExceptionfor an unknown source part; a mistyped source now surfaces atvalidate()or save time as a missing source part. - Breaking:
RelationshipsandRelationshiphold their package weakly. Operations that need it, such asgetTargetPart(),create(), andretarget(), throw once the package has been released instead of keeping it alive. - Relationship parts are serialized once when read or saved instead of after every change, changed collections stay live between calls, and generated ids no longer rescan from
rId1on every addition. Adding 800 package relationships dropped from about 570 ms to about 2 ms. - Part-name availability checks use an indexed exact, ancestor, and descendant lookup instead of scanning every package entry for each added part.
- Successful saves reuse the verified temporary package fingerprint after atomic replacement instead of hashing the same output a second time.
Fixed
- New packages write
[Content_Types].xmlas the first ZIP entry, as OPC expects for streaming readers, instead of appending it after every part. hasPart()returnsfalsefor package metadata and invalid OPC part names instead of throwing while performing an existence check.
See the full changelog.
v0.6.0
Added
- Lazy streams for unchanged ZIP-backed parts, sharing one source archive whose container remains alive until the caller closes the last stream.
- Deferred part paths using native
zip://URIs for unchanged entries and package-owned local materialization for staged content and path-only consumers. - Local path-based APIs for adding and replacing large parts without first loading their contents into PHP strings.
- Same-process coordination of source archives before atomic replacement.
Changed
- Source files are fully hashed once when opened and immediately before an in-place save; part reads use inexpensive filesystem metadata checks.
openStream()now returns a lazy ZIP stream for unchanged parts. Seekability is no longer guaranteed; usegetLocalPath()when random access is required.- Source archives remain open for the package lifetime and are shared by its streams. Replacing a source is rejected while a relevant stream remains open.
- Package opening and small-part reads no longer repeatedly hash the complete source package.
See the full changelog.
v0.5.0
Added
- Structural OPC digital-signature inspection and explicit removal for intentionally unsigned copies.
- Agile Encryption reading for AES-128/192/256 with SHA-1/256/384/512 and hardened encrypted-input limits.
- Strict ECMA-376 part-name validation, relationship resolution, inbound-reference inspection, safe part removal and moves, and explicit package repair APIs.
- Windows, PHP 8.1 lowest-dependency, LibreOffice interoperability, and package benchmark coverage.
Changed
- Renamed the Composer package to
dkulyk/openxml-package;dkulyk/openxmlremains replaced for dependency compatibility. - Reorganized documentation and streamlined CI execution.
Fixed
- Corrected platform-independent relationship paths, move-over-removed-part saves, created-file permissions, percent-encoded Unicode part names, and live relationship collection reuse.
See the full changelog.
v0.4.0
Added
- Lazy package opening that retains ZIP metadata without loading every part.
- Stream APIs for adding, reading, and replacing large binary parts.
- Temporary-file staging for streamed writes.
- ZIP copy-through for unchanged compressed entries.
- Coverage for stream round trips, limits, rollback, memory use, copy-through metadata, and concurrent source changes.
Changed
- Updated GitHub Actions and moved code-quality checks into one PHP 8.1 job.
- Packages reopen their container after an atomic save, keeping lazy reads pinned to the new source.
See the full changelog.