Skip to content

Releases: go-odf/odf

v0.7.0 — a slide holds shapes, and the words are inside them

Choose a tag to compare

@tannevaled tannevaled released this 10 Oct 20:41
3d1deee

Parse now reads ODP presentations and ODG drawings, which are the same
document: office:presentation and office:drawing both hold draw:page
elements, and a page holds shapes with the words inside them. Walking
office:text found nothing in either — the same silence an .ods gave before
office:spreadsheet was handled.

Each page becomes a heading of its own draw:name followed by the blocks found
in its shapes, so a reader can tell which slide a sentence was on.

⛔ Every rule was read off a file LibreOffice WROTE

The fixtures are files Impress and Draw produced. Four rules came out the
other way from the specification
, and the sharpest is the one the
specification suggests:

the title is not presentation:class="title" Impress strips that attribute from every frame it does not treat as a master-page placeholder, so a converted deck has no classed title frame at all. The heading is the page's own draw:name, which survives a conversion and is always there.
presentation:notes is skipped it holds a frame whose text reads exactly like body text, so walking into it puts words the audience never saw into the document.
a picture is held back Impress writes a draw:image preview of a table into the same frame as the table — Pictures/TablePreview1.svm. A picture is emitted only if its shape held nothing else.
svg:title/svg:desc are the frame's children beside draw:image, not inside it; a reader looking within finds none, and a slide's picture arrives with no alternative text.

The shape types are deliberately not enumerated: ODF has dozens and any of
them may carry text, so the walk descends and collects what it meets.
draw:g, draw:caption and draw:custom-shape work without being named.

TestNoFrameOnARealDeckIsClassedTitle is the witness for the heading rule
rather than a restatement of it: without it the specification's rule could be
reintroduced and every other test would still pass.

maxPages (65 536) joins the ceilings, so a file claiming four million pages
is refused rather than walked.

100 % statement coverage, race clean, go vet and gofmt clean. Four
mutations — notes no longer skipped, the picture no longer held back, the
ceiling raised, the page name dropped — are all caught.

v0.6.0 — bound what a package can ask for

Choose a tag to compare

@tannevaled tannevaled released this 09 Oct 13:32
780b61c

A security release. Behaviour changes: packages this reader previously
accepted are now refused, which is why it is a minor rather than a patch.

An ODF package is a ZIP, and a ZIP says how big it will be once opened.
What it says is a number in a file.

input before after
an .ods of 67 206 bytes whose content.xml inflates to 64 MiB 222.9 MiB held, accepted 0.0 MiB, refused by its declared size
a 65 851 byte package of 200 000 nested tables 115.5 MiB, 296 ms, accepted 18.0 MiB, 7 ms, refused

Four ceilings, bounding four different things

any one entry 64 MiB refused from its declared size, before a byte is inflated
the package 192 MiB one entry under the ceiling says nothing about a thousand
entries 8 192 the count decides an allocation made before anything is read
nesting 256 no size ceiling can see it: the bytes are small, the shape costs

For scale, OpenSSL's own lifecycles.ods — six sheets, every row padded to
1024 columns — has a 96 453 byte content.xml. These leave it six hundred
times its size.

⛔ Where a guard goes is not where the code recurses

The nesting bound was first written where this package recurses. It caught
nothing: a table inside a table cell never reaches parseBlock, because
parseCell hands it to the decoder's Skip. The memory was never in our
document tree — it was in encoding/xml's stack of open elements.

⛔ Two guards were removed rather than kept

A check on the bytes actually read is unreachable beside the declared-size
check: archive/zip makes the declaration binding. An unreachable guard is not
defence in depth; it is a claim nobody has tested. The dependency that replaces
it is pinned by a test.

Measured and found NOT to be risks

encoding/xml expands no custom entities and fetches no external ones, so
billion laughs and <!ENTITY x SYSTEM "file:///etc/passwd"> are refused by
the decoder itself — measured in both directions rather than assumed.

100 % statement coverage, -race green, ten build targets.

v0.5.0 — ODS spreadsheets

Choose a tag to compare

@tannevaled tannevaled released this 08 Oct 17:53
cd0b36f

Read ODS spreadsheets, not just ODT

Parse now accepts an .ods package. A spreadsheet is the same container
as a text document — the same ZIP, the same content.xml, the same
table:table/-row/-cell elements — inside an office:spreadsheet body
instead of an office:text one. Each sheet comes back as a level-1 Heading
carrying its table:name, followed by the sheet's Table.

Write still always produces ODT. This is additive — nothing about the ODT
round-trip changed, which is why it is a minor.

Why this was worse than a missing feature

⛔ The cost was silent. An .ods parsed without an error into a document
of no blocks, so a conversion handed somebody an empty page and no reason for
it. An empty answer and a refusal are different things, and this was neither.

A repeat is a count in a FILE

table:number-columns-repeated and -rows-repeated are honoured — and capped,
at 1024 columns and 65536 rows, because materialising what they ask for is an
allocation the input decides. The trailing padding every spreadsheet writes
to square its rows off is dropped, but only from the end: an empty cell
between two full ones is a gap in the data.

OpenSSL's own lifecycles.ods pads to 1024 columns and asks for 1 048 559
repeated empty rows
. Read as written, its six sheets are 91 rows and 14
columns.

Measured on real files

Four third-party .ods written by real spreadsheets: 4 read, 0 refused, 0
empty. End to end through go-richdoc/latex/pdf, OpenSSL's workbook typesets
to a 5-page PDF whose six sheet names poppler — a judge independent of this
stack — finds as numbered headings.

100% statement coverage, go vet and gofmt clean, 11 checks green across
10 architectures, and 10 mutations against the new decisions all caught.