opc

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: BSD-3-Clause Imports: 8 Imported by: 1

README

opc

The Open Packaging Conventions: the ZIP, the content types and the relationships that .docx, .xlsx, .pptx and .xps are all built on. Pure Go, CGO-free, including GOOS=js. No dependencies outside the standard library.

p, err := opc.Open(bytes, opc.Limits{})
main, err := p.Main()              // "word/document.xml", "xl/workbook.xml", …
doc, err := p.Part(main)
rels, err := p.Rels(main)          // what that part points at
target, err := p.Resolve(main, rels[0])

and the other direction:

b, err := opc.Build([]opc.NewPart{
    {Name: "word/document.xml", ContentType: wordMain, Data: doc},
    {Name: "word/styles.xml", Data: styles},      // .xml already has a Default
}, map[string][]opc.Rel{
    "":                  {{ID: "rId1", Type: opc.RelOfficeDocument, Target: "word/document.xml"}},
    "word/document.xml": {{ID: "rId1", Type: relStyles, Target: "styles.xml"}},
})

It is the layer below a format reader or writer. What it knows is how to find a part and what a part says it is; what the parts mean is WordprocessingML's business, or SpreadsheetML's, or PresentationML's.

Building one

Build writes the content types part and every relationships part itself, because where those go is a rule and not a choice — _rels/.rels beside the package, word/_rels/document.xml.rels beside the part. It refuses the two names it writes, since a second copy would contradict the first.

It also refuses four packages that would open and then fail to say anything, in whatever program the file was written for rather than here:

a part name with .. in it which escapes the package
the same part twice where the second would be unreachable
a relationship from a part that is not there a dangling edge nothing can resolve
no package relationships at all a package nothing points into has no main part, and that is where every reader starts

⛔ The same package twice is the same bytes. A map walk is random, and a writer whose output changes between runs cannot be diffed, cached or signed. The test builds eight relationship sources and compares five times: with three sources a map walk comes out in the right order one time in six, and the test reported a determinism that was not there.

⛔ Two things are asserted by their spelling rather than by being read back, which is the opposite of how everything else here is checked: an Override's PartName starts with a slash, and the .rels Default is present. Our own reader accepts a package without either, so reading it back proves nothing — and the programs that refuse such a package are the ones nothing here can run. The control is a .docx Word itself produced, which carries both.

⛔ The control for the writer is the reader

Every other test builds a package and then opens it with Open. A writer checked against a string of expected XML is checked against what its author believed; the thing that has to be true is that it can be read.

⛔ Nothing here is found by its path

The main part of a package is reached through a relationship, never by guessing a filename. Measured on three files LibreOffice wrote in the same minute, the one relationship type that names it points at three different places:

the officeDocument relationship targets
.docx word/document.xml
.xlsx xl/workbook.xml
.pptx ppt/presentation.xml

A reader that hardcoded word/document.xml would work on most Word documents and on nothing else — and would still fail on the Word documents whose producer chose another name, which the standard allows.

⛔ A relationship type is compared without regard to case

Not caution: a measurement. In the .xlsx of those three, the core-properties relationship type came out as

http://schemas.openxmlformats.org/officedocument/2006/relationships/...

with officedocument in lower case, while the .docx and the .pptx from the same program, the same version and the same minute wrote officeDocument. A producer is inconsistent about the case of its own URIs, so a reader that compares them exactly works on two files out of three. SameRelType is the comparison, and TestARealProducerWroteARelationshipTypeInTheWrongCase fails if a future fixture no longer shows it — so the rule cannot quietly become folklore.

What a target is relative to

A relationship's target is relative to the directory of the part that declared it. From word/document.xml:

target resolves to
styles.xml word/styles.xml
../docProps/app.xml docProps/app.xml
/docProps/core.xml docProps/core.xml
media\image1.png word/media/image1.png

⛔ An external target is an error rather than a path. http://example.com/x cleaned into a part name becomes http:/example.com/x, which is a lookup that fails for a reason with nothing to do with what went wrong.

⛔ A target that climbs out of the package is refused rather than cleaned into something harmless. This package writes no files, so there is no zip-slip to suffer here — but it hands part names to its callers, and a caller that joined one onto a directory would.

What a package may ask for

An OPC container is a ZIP, and a ZIP says how big it will be once opened. What it says is a number in a file: it costs the author of that file nothing to claim four gigabytes, and a reader that believes it allocates them.

default why
one part, opened 64 MiB refused from its declared size, before a byte is inflated — then the read is bounded again, in case the declaration was a lie the other way
every part together 384 MiB one part under the ceiling says nothing about a thousand of them
the file itself 256 MiB
parts 8 192 it decides an allocation made before anything is read
relationships per part 4 096 the same, from a count the file chose

The bomb in the tests is measured, not asserted: 8 638 bytes on disk open to 8 MiB, a ratio of 971:1, and the test fails if the fixture stops being a bomb — so a change that made it smaller would be visible rather than making the test merely look stricter.

What is deliberately not fatal

no [Content_Types].xml the package is malformed and every real reader opens it anyway: the parts are all still there. Refusing would turn "I cannot tell you what this part is" into "I cannot read this file"
a directory entry, or an odd name not a part; skipped. Refusing a whole file because of one would make a reader that fails where every other reader opens — while still never handing the name to a caller
a part with no relationships most parts of most packages have none

Checks

100 % statement coverage with zero uncovered blocks, go vet and gofmt clean, race-clean, and it builds for ten targets including js/wasm, linux/loong64 and linux/s390x.

⛔ The coverage gate reads the profile, not the total line. go tool cover -func prints one decimal, so a single uncovered statement out of two thousand rounds to 100.0% and a gate reading that total passes. This one fails on any block whose execution count is zero, and the gate itself was tested in both directions before being trusted.

Licence

BSD-3-Clause.

Documentation

Overview

Package opc reads an Open Packaging Conventions container: the ZIP, the content types and the relationships that docx, xlsx, pptx and xps are all built on.

It is the layer below a format reader. What it knows is how to find a part and what a part says it is; what the parts MEAN is WordprocessingML's business, or SpreadsheetML's, or PresentationML's.

Nothing here is found by its path

⛔ The main part of a package is reached through a RELATIONSHIP, never by guessing a filename. Measured on files LibreOffice wrote, the one relationship type that names it points at three different places:

.docx → word/document.xml
.xlsx → xl/workbook.xml
.pptx → ppt/presentation.xml

A reader that hardcoded "word/document.xml" would work on most Word documents and on nothing else, and would still fail on the Word documents whose producer chose another name — which the standard allows.

Index

Constants

View Source
const (
	// RelOfficeDocument names the package's main part: the Word document, the
	// Excel workbook, the PowerPoint presentation.
	RelOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
	// RelCoreProperties names the Dublin Core metadata part.
	RelCoreProperties = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
)

The relationship types this package knows by name. A type is the whole URI, and it is compared with SameRelType rather than with ==.

Variables

This section is empty.

Functions

func Build

func Build(parts []NewPart, rels map[string][]Rel) ([]byte, error)

Build writes an OPC container: the parts, the content types that say what each is, and the relationships that say how anything is found.

rels maps a part name to the relationships that start AT it. The empty name is the package's own, which goes in _rels/.rels and is where a reader starts — see Package.Main. The relationships parts are written for you, in the place Package.Rels looks for them, because that place is a rule and not a choice.

⛔ It refuses rather than writing a container that cannot be read back:

a part name with ".." in it      which escapes the package
two parts with the same name     where the second would be unreachable
[Content_Types].xml, or a .rels  which this writes itself, and a second
                                 copy would contradict the first
a relationship from a part       that is not in the package, which is a
                                 dangling edge nothing can resolve
no package relationships         because a package nothing points into has
                                 no main part, and every reader starts there

Each of those is a container that opens and then fails to say anything, and the failure turns up in whatever program the file was written for rather than here.

func SameRelType

func SameRelType(a, b string) bool

SameRelType says whether two relationship type URIs mean the same thing.

⛔ It is NOT a string comparison, and this is measured rather than cautious. In one .xlsx that LibreOffice wrote, the core-properties relationship type came out as

http://schemas.openxmlformats.org/officedocument/2006/relationships/...

with "officedocument" in lower case, while the .docx and .pptx from the same program, the same version and the same minute wrote "officeDocument". A producer is inconsistent about the case of its own URIs, so a reader that compares them exactly works on two files out of three.

func TooLarge

func TooLarge(err error) bool

TooLarge says whether an error is a ceiling refusing a file, so that a caller can tell "this file is hostile or enormous" from "this file is broken" without reading the message.

Types

type Limits

type Limits struct {
	// MaxPartBytes is how large one part may be, opened. Zero means 64 MiB.
	MaxPartBytes int64
	// MaxPackageBytes is how large the file itself may be. Zero means 256 MiB.
	MaxPackageBytes int64
	// MaxUncompressedBytes is how large every part may come to TOGETHER. Zero
	// means 384 MiB.
	//
	// ⛔ One part under the ceiling says nothing about a thousand of them,
	// which is the shape of bomb a per-part limit alone invites: a package of
	// eight thousand parts each a byte under the limit.
	MaxUncompressedBytes int64
	// MaxParts is how many entries the ZIP may hold. Zero means 8192.
	//
	// It is checked before the parts are indexed, because that index is an
	// allocation made from a count the file chose.
	MaxParts int
	// MaxRels is how many relationships one part may declare. Zero means 4096.
	MaxRels int
}

Limits bound what a package may ask for before anything is allocated.

An OPC container is a ZIP, and a ZIP says how big it will be once opened. ⛔ What it says is a number in a FILE: it costs the author of that file nothing to claim four gigabytes, and a reader that believes it allocates them. Every ceiling here is checked against the DECLARED size first, so a refusal happens before a byte is inflated, and then the read itself is bounded again in case the declaration was a lie in the other direction.

The zero value asks for the defaults, which are generous for real documents and small for hostile ones. They are the same ceilings go-odf/odf arrived at after measuring what a bomb costs: a 67 206-byte spreadsheet whose content.xml inflates to 64 MiB parsed without complaint and held 222.9 MiB — a thousandfold on disk, and better than three times the inflated size in the heap, because the bytes are read, decoded as UTF-8, then kept as runs.

type NewPart

type NewPart struct {
	// Name is the part name, with or without a leading slash — "/" is how the
	// content types part spells one and the ZIP does not, and a caller should
	// not have to remember which this is.
	Name string

	// ContentType is what the content types part will say this is. It may be
	// empty for a part whose extension already has a Default: every .rels and
	// every .xml part does, which is why most packages override only a few.
	ContentType string

	// Data is the part's bytes.
	Data []byte
}

A NewPart is one part of a package being built: where it goes, what it is, and what is in it.

type Package

type Package struct {
	// contains filtered or unexported fields
}

A Package is an OPC container opened for reading. It is safe for concurrent reads.

func Open

func Open(b []byte, lim Limits) (*Package, error)

Open reads a package from bytes.

func (*Package) ContentType

func (p *Package) ContentType(name string) string

ContentType says what a part declares itself to be: its Override if it has one, otherwise the Default for its extension, otherwise "".

func (*Package) Has

func (p *Package) Has(name string) bool

Has says whether a part is in the package, without reading it.

func (*Package) Main

func (p *Package) Main() (string, error)

Main returns the name of the package's main part: the Word document, the Excel workbook, the PowerPoint presentation.

⛔ This is the whole reason this package exists. The part is found through the officeDocument relationship in _rels/.rels, and the three formats put it in three different places — word/document.xml, xl/workbook.xml, ppt/presentation.xml — so a path cannot be guessed even if a producer always chose the usual one, which it need not.

func (*Package) Part

func (p *Package) Part(name string) ([]byte, error)

Part returns a part's bytes.

The lookup is exact first and case-insensitive second, because OPC compares part names without regard to case while a ZIP does not: a package whose [Content_Types].xml says "/word/Document.xml" and whose entry is "word/document.xml" is one file, and both names have to find it.

func (*Package) Parts

func (p *Package) Parts() []string

Parts lists every part in the package, in the order the ZIP holds them.

func (*Package) RelOfType

func (p *Package) RelOfType(part, relType string) (Rel, bool, error)

RelOfType returns the first relationship of a part with the given type.

⛔ "First", and the order is the file's. A package may name two parts with the same relationship type — several slides, several worksheets — and for those the caller wants Package.Rels and all of them. This is for the types of which there is one: the main part, the core properties.

func (*Package) Rels

func (p *Package) Rels(part string) ([]Rel, error)

Rels returns the relationships of a part. An empty name asks for the package's own, at _rels/.rels.

A part with no relationships part has no relationships, which is not an error: most parts of most packages have none.

func (*Package) Resolve

func (p *Package) Resolve(part string, r Rel) (string, error)

Resolve turns a relationship's target into a part name of this package.

A target is relative to the DIRECTORY of the part that declared it — from word/document.xml, "styles.xml" is word/styles.xml and "../docProps/app.xml" is docProps/app.xml. A target beginning with a slash is from the root of the package instead.

⛔ An external target is an error rather than a path. "http://example.com/x" cleaned into a part name becomes "http:/example.com/x", which is a lookup that fails for a reason that has nothing to do with what went wrong.

type Rel

type Rel struct {
	ID     string
	Type   string
	Target string
	// External is true when Target is a URI rather than a part of this
	// package. ⛔ It must be checked before Target is used as a part name: an
	// external target is a link to the web, and treating it as a path is how a
	// reader ends up asking the package for "https:/example.com/x".
	External bool
}

A Rel is one relationship: a named edge from a part (or from the package) to somewhere else.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL