diff --git a/filter.go b/filter.go index c26248b..4747617 100644 --- a/filter.go +++ b/filter.go @@ -315,25 +315,69 @@ func intParm(parm Dict, key Name, def int, r Resolver) int { // a prefix comes back with the error that ended it, never dressed up as a whole // stream, so the caller can tell the difference. func flateDecode(data []byte) ([]byte, error) { - i := 0 - for i < len(data) && isSpace(data[i]) { - i++ + // Where the stream really starts is ambiguous, and the ambiguity has to be + // resolved by reading rather than by guessing. + // + // Producers put the stream's EOL before the data, so leading white-space is + // skipped. But NUL is one of the six bytes the specification calls + // white-space, and a bare deflate stream whose first block is STORED begins + // with a NUL. Skip that and the stream does not merely fail: 00 03 00 fc ff + // "abc" reads, once its NUL is gone, as a fixed-Huffman final block that + // ends at once, so it inflates to NOTHING with no error. Silence, not a + // diagnostic. + // + // Stored blocks are ordinary -- they are what a deflater emits for data + // that will not compress, which in a PDF is every image that arrived + // already compressed -- and "\r\n" followed by such a block needs exactly + // two bytes skipped, not three. So every split point inside the leading + // white-space run is tried, and the read that actually yields bytes wins. + ws := 0 + for ws < len(data) && isSpace(data[ws]) { + ws++ } - data = data[i:] - if zr, err := zlib.NewReader(bytes.NewReader(data)); err == nil { - out, err := readAllCapped(zr) - if err == nil { - return out, nil + + var best []byte + var bestErr error + have := false + + try := func(b []byte, wrapped bool) bool { + var out []byte + var err error + if wrapped { + zr, zerr := zlib.NewReader(bytes.NewReader(b)) + if zerr != nil { + return false + } + out, err = readAllCapped(zr) + } else { + out, err = readAllCapped(flate.NewReader(bytes.NewReader(b))) } - if len(out) > 0 { - return out, fmt.Errorf("reader: FlateDecode: %w", err) + if err == nil && len(out) > 0 { + best, bestErr, have = out, nil, true + return true } + if !have || len(out) > len(best) { + best, bestErr, have = out, err, true + } + return false } - out, err := readAllCapped(flate.NewReader(bytes.NewReader(data))) - if err != nil { - return out, fmt.Errorf("reader: FlateDecode: %w", err) + + // zlib is tried only past the white-space, because a valid zlib stream + // cannot begin inside it: RFC 1950 fixes CM to 8, so the low nibble of the + // first byte is 8, and none of 00, 09, 0a, 0c, 0d, 20 has a low nibble of 8. + if try(data[ws:], true) { + return best, nil + } + // Bare deflate, from the far end of the white-space run back to the start. + for i := ws; i >= 0; i-- { + if try(data[i:], false) { + return best, nil + } } - return out, nil + if bestErr != nil { + return best, fmt.Errorf("reader: FlateDecode: %w", bestErr) + } + return best, nil } // readAllCapped reads r, refusing to grow past maxDecodedSize. diff --git a/filter_test.go b/filter_test.go index e966dd5..a75bccd 100644 --- a/filter_test.go +++ b/filter_test.go @@ -311,3 +311,45 @@ func TestDecodeFlateFailure(t *testing.T) { t.Error("want an error") } } + +// A bare deflate stream whose first block is STORED begins with a NUL, and NUL +// is one of the six bytes the specification calls white-space. Skipping it eats +// a real byte and the stream dies one byte in. +// +// The stream is built by hand rather than by a deflater, because which block +// type a deflater picks is its own business and changes between releases: Go +// 1.26 compressed this test's data, Go 1.27 stored it, and that is how this was +// found. +func TestFlateDecodeStoredBlockStartingWithNUL(t *testing.T) { + want := []byte("abc") + + var raw []byte + // Non-final stored block: BFINAL=0, BTYPE=00, then LEN and ^LEN, little-endian. + raw = append(raw, 0x00, byte(len(want)), 0x00, ^byte(len(want)), 0xff) + raw = append(raw, want...) + // Final empty stored block. + raw = append(raw, 0x01, 0x00, 0x00, 0xff, 0xff) + + if raw[0] != 0x00 { + t.Fatalf("test is not exercising what it claims: first byte %#x, want NUL", raw[0]) + } + if !isSpace(raw[0]) { + t.Fatal("test is not exercising what it claims: NUL is not treated as white-space") + } + + got, err := flateDecode(raw) + if err != nil || !bytes.Equal(got, want) { + t.Errorf("stored block: got %q, %v; want %q", got, err, want) + } +} + +// A bare deflate stream preceded by the stream's EOL: here the white-space skip +// is the thing that makes it readable, which is why it is there. +func TestFlateDecodeRawAfterLeadingEOL(t *testing.T) { + want := []byte("some data worth compressing, twice over") + padded := append([]byte("\r\n"), rawDeflateBytes(t, want)...) + got, err := flateDecode(padded) + if err != nil || !bytes.Equal(got, want) { + t.Errorf("raw deflate after EOL: got %q, %v", got, err) + } +} diff --git a/recover_test.go b/recover_test.go index a5c6b22..1018f25 100644 --- a/recover_test.go +++ b/recover_test.go @@ -105,8 +105,25 @@ func TestDecodeRecoveringDamagedFlateKeepsPredictor(t *testing.T) { if len(dec.Data) < 4*columns { t.Fatalf("recovered only %d bytes", len(dec.Data)) } - if !bytes.HasPrefix(want, dec.Data) { - t.Errorf("predictor not undone over the prefix: got %v, want a prefix of %v", dec.Data[:8], want[:8]) + // Every row but the last is true. The last one may not be: the stream can + // end mid-row, and pngPredictor deliberately pads the remainder with zeroes + // and emits the row rather than dropping it -- TestPNGPredictorTruncatedRow + // pins that. So the row the damage landed in is excluded here. + // + // Asserting over the whole of dec.Data only ever passed because the + // truncation happened to fall on a row boundary. Go 1.27's deflater picks + // different block types for this data than 1.26's, the halfway cut moved, + // and the assertion failed on a library that had not changed. + whole := len(dec.Data) - columns + if whole < 0 { + whole = 0 + } + if !bytes.HasPrefix(want, dec.Data[:whole]) { + n := whole + if n > 8 { + n = 8 + } + t.Errorf("predictor not undone over the whole rows: got %v, want a prefix of %v", dec.Data[:n], want[:n]) } }