// SPDX-License-Identifier: GPL-3.0-or-later package extract import ( "archive/zip" "bytes" "errors" "strings" "testing" ) // zipFile builds an archive from name -> content pairs and writes it to disk. func zipFile(t *testing.T, name string, entries map[string]string) string { t.Helper() var buf bytes.Buffer w := zip.NewWriter(&buf) for n, c := range entries { f, err := w.Create(n) if err != nil { t.Fatal(err) } if _, err := f.Write([]byte(c)); err != nil { t.Fatal(err) } } if err := w.Close(); err != nil { t.Fatal(err) } return file(t, name, buf.Bytes()) } func TestZipFormats(t *testing.T) { e := newWithPath("") tests := []struct { name string entries map[string]string want []string }{ {"inv.docx", map[string]string{ "word/document.xml": `Acme LtdFakturaVAT`, "word/footer1.xml": `NIP 0000000000`, "word/styles.xml": `NOT-INCLUDED`, }, []string{"Acme Ltd", "Faktura VAT", "NIP 0000000000"}}, {"sheet.xlsx", map[string]string{ "xl/sharedStrings.xml": `Invoiceacme ltd`, "xl/worksheets/sheet1.xml": `01234567890inline text`, }, []string{"Invoice", "acme ltd", "1234567890", "inline text"}}, {"deck.pptx", map[string]string{ "ppt/slides/slide1.xml": `Quarterly report`, }, []string{"Quarterly report"}}, {"letter.odt", map[string]string{ "content.xml": `FakturaVAT`, "styles.xml": `header acme`, }, []string{"Faktura VAT", "header acme"}}, {"book.epub", map[string]string{ "OEBPS/ch1.xhtml": `

Chapter one&two

`, "mimetype": `application/epub+zip`, }, []string{"Chapter one&two"}}, } for _, tt := range tests { got, err := text(t, e, zipFile(t, tt.name, tt.entries), 0) if err != nil { t.Errorf("%s: %v", tt.name, err) continue } for _, w := range tt.want { if !strings.Contains(got, w) { t.Errorf("%s: text %q lacks %q", tt.name, got, w) } } if strings.Contains(got, "NOT-INCLUDED") { t.Errorf("%s: read an entry it should skip: %q", tt.name, got) } } } func TestSharedStringIndexNotText(t *testing.T) { e := newWithPath("") got, err := text(t, e, zipFile(t, "s.xlsx", map[string]string{ "xl/sharedStrings.xml": `alpha`, "xl/worksheets/sheet1.xml": `987654`, }), 0) if err != nil { t.Fatal(err) } if strings.Contains(got, "987654") { t.Fatalf("shared-string index leaked into text: %q", got) } } func TestZipCorruptAndBudget(t *testing.T) { e := newWithPath("") if _, err := text(t, e, file(t, "broken.docx", []byte("not a zip at all")), 0); err == nil || errors.Is(err, ErrUnsupported) { t.Errorf("corrupt docx: %v, want a zip error", err) } old := zipBudget zipBudget = 1 << 10 defer func() { zipBudget = old }() big := `` + strings.Repeat("x", 4096) + `` if _, err := text(t, e, zipFile(t, "huge.docx", map[string]string{"word/document.xml": big}), 0); !errors.Is(err, ErrTooLarge) { t.Errorf("over budget: %v, want ErrTooLarge", err) } } // TestOdfEmbeddedObjectIncluded: an ODF embedded object (a chart, a // formula) keeps its own content.xml inside a subdirectory such as // "Object 1/"; matchEntry's base-name fallback picks it up alongside the // document's own content.xml, deliberately — that embedded text is text // the document shows its reader. func TestOdfEmbeddedObjectIncluded(t *testing.T) { e := newWithPath("") got, err := text(t, e, zipFile(t, "embed.odt", map[string]string{ "content.xml": `cover page`, "Object 1/content.xml": `embedded chart acme`, }), 0) if err != nil { t.Fatal(err) } if !strings.Contains(got, "embedded chart acme") { t.Errorf("embedded object text missing: %q", got) } } // TestZipLenientOnMalformedEntry: a malformed entry must not blank out // text already read from a good entry in the same archive, and the text // the malformed entry itself yielded before its own error is kept too. // The footer's mismatched end tag ( where was open) // genuinely fails to parse under Strict=false — unlike a merely missing // end tag, which the decoder synthesises and swallows — but only after // "broken" and the newline for have already been written; the // XML decoder itself confirms this token by token (see fix round 2's // report for the trace): CharData "broken", EndElement p, then the // error "unexpected end element ". func TestZipLenientOnMalformedEntry(t *testing.T) { e := newWithPath("") got, err := text(t, e, zipFile(t, "partial.docx", map[string]string{ "word/document.xml": `good body text`, "word/footer1.xml": `broken`, }), 0) // Partly read: the text is kept, and ErrPartial says some of it is // missing, so a keyword not found in it is unknown (plan 11). if !errors.Is(err, ErrPartial) { t.Fatalf("good entry alongside a malformed one: err %v, want ErrPartial", err) } if !strings.Contains(got, "good body text") { t.Errorf("text from the good entry was discarded: %q", got) } if !strings.Contains(got, "broken") { t.Errorf("text the malformed entry yielded before its own error was discarded: %q", got) } } // TestZipMalformedOnlyEntryErrors: when the only matched entry is // malformed, no text survives to return, so the error is reported instead // — named after the entry it came from. The malformed attribute syntax // breaks the decode before any character data is ever emitted, so there // is nothing for the lenient path (TestZipLenientOnMalformedEntry) to // keep. func TestZipMalformedOnlyEntryErrors(t *testing.T) { e := newWithPath("") _, err := text(t, e, zipFile(t, "bad.docx", map[string]string{ "word/document.xml": `) must not count as "text was // found" and mask a failing sibling's error — the separator newline // zipText appends after every successful entry means b.Len() alone // cannot answer "did anything write text"; only word/footer1.xml's own // contribution (zero, since it fails while still inside its opening tag, // before any token is emitted) may be counted, and it contributed // nothing either, so the archive as a whole produced no text and the // wrapped error must surface. func TestZipNoTextAndFailingSiblingErrors(t *testing.T) { e := newWithPath("") _, err := text(t, e, zipFile(t, "empty.docx", map[string]string{ "word/document.xml": ``, "word/footer1.xml": `` + strings.Repeat("x", 4096) + `` p := zipFile(t, "huge.docx", map[string]string{"word/document.xml": big}) if _, err := text(t, e, p, 1024); !errors.Is(err, ErrTooLarge) { t.Fatalf("got %v, want ErrTooLarge (max-read 1024 should have capped a 4096-byte entry)", err) } }