diff options
author | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 13:34:23 -0600 |
---|---|---|
committer | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 13:34:23 -0600 |
commit | 6438df3a1e0fb944485cebf07976160184697d72 (patch) | |
tree | 00b09bfd170e77ae9391b1a2f5a93ef6839f2597 /workhorse/cmd | |
parent | 42bcd54d971da7ef2854b896a7b34f4ef8601067 (diff) | |
download | gitlab-ce-6438df3a1e0fb944485cebf07976160184697d72.tar.gz |
Add latest changes from gitlab-org/gitlab@13-8-stable-eev13.8.0-rc42
Diffstat (limited to 'workhorse/cmd')
-rw-r--r-- | workhorse/cmd/gitlab-resize-image/main.go | 9 | ||||
-rw-r--r-- | workhorse/cmd/gitlab-resize-image/png/reader.go | 86 | ||||
-rw-r--r-- | workhorse/cmd/gitlab-resize-image/png/reader_test.go | 97 |
3 files changed, 191 insertions, 1 deletions
diff --git a/workhorse/cmd/gitlab-resize-image/main.go b/workhorse/cmd/gitlab-resize-image/main.go index 662efd7306d..81288c645eb 100644 --- a/workhorse/cmd/gitlab-resize-image/main.go +++ b/workhorse/cmd/gitlab-resize-image/main.go @@ -7,6 +7,8 @@ import ( "strconv" "github.com/disintegration/imaging" + + "gitlab.com/gitlab-org/gitlab-workhorse/cmd/gitlab-resize-image/png" ) func main() { @@ -23,7 +25,12 @@ func _main() error { return fmt.Errorf("GL_RESIZE_IMAGE_WIDTH: %w", err) } - src, formatName, err := image.Decode(os.Stdin) + pngReader, err := png.NewReader(os.Stdin) + if err != nil { + return fmt.Errorf("construct PNG reader: %w", err) + } + + src, formatName, err := image.Decode(pngReader) if err != nil { return fmt.Errorf("decode: %w", err) } diff --git a/workhorse/cmd/gitlab-resize-image/png/reader.go b/workhorse/cmd/gitlab-resize-image/png/reader.go new file mode 100644 index 00000000000..8229454ee3b --- /dev/null +++ b/workhorse/cmd/gitlab-resize-image/png/reader.go @@ -0,0 +1,86 @@ +package png + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "os" +) + +const ( + pngMagicLen = 8 + pngMagic = "\x89PNG\r\n\x1a\n" +) + +// Reader is an io.Reader decorator that skips certain PNG chunks known to cause problems. +// If the image stream is not a PNG, it will yield all bytes unchanged to the underlying +// reader. +// See also https://gitlab.com/gitlab-org/gitlab/-/issues/287614 +type Reader struct { + underlying io.Reader + chunk io.Reader + bytesRemaining int64 +} + +func NewReader(r io.Reader) (io.Reader, error) { + magicBytes, err := readMagic(r) + if err != nil { + return nil, err + } + + if string(magicBytes) != pngMagic { + debug("Not a PNG - read file unchanged") + return io.MultiReader(bytes.NewReader(magicBytes), r), nil + } + + return io.MultiReader(bytes.NewReader(magicBytes), &Reader{underlying: r}), nil +} + +func (r *Reader) Read(p []byte) (int, error) { + for r.bytesRemaining == 0 { + const ( + headerLen = 8 + crcLen = 4 + ) + var header [headerLen]byte + _, err := io.ReadFull(r.underlying, header[:]) + if err != nil { + return 0, err + } + + chunkLen := int64(binary.BigEndian.Uint32(header[:4])) + if chunkType := string(header[4:]); chunkType == "iCCP" { + debug("!! iCCP chunk found; skipping") + if _, err := io.CopyN(ioutil.Discard, r.underlying, chunkLen+crcLen); err != nil { + return 0, err + } + continue + } + + r.bytesRemaining = headerLen + chunkLen + crcLen + r.chunk = io.MultiReader(bytes.NewReader(header[:]), io.LimitReader(r.underlying, r.bytesRemaining-headerLen)) + } + + n, err := r.chunk.Read(p) + r.bytesRemaining -= int64(n) + return n, err +} + +func debug(args ...interface{}) { + if os.Getenv("DEBUG") == "1" { + fmt.Fprintln(os.Stderr, args...) + } +} + +// Consume PNG magic and proceed to reading the IHDR chunk. +func readMagic(r io.Reader) ([]byte, error) { + var magicBytes []byte = make([]byte, pngMagicLen) + _, err := io.ReadFull(r, magicBytes) + if err != nil { + return nil, err + } + + return magicBytes, nil +} diff --git a/workhorse/cmd/gitlab-resize-image/png/reader_test.go b/workhorse/cmd/gitlab-resize-image/png/reader_test.go new file mode 100644 index 00000000000..2b2d5df24e9 --- /dev/null +++ b/workhorse/cmd/gitlab-resize-image/png/reader_test.go @@ -0,0 +1,97 @@ +package png + +import ( + "bytes" + "hash/crc64" + "image" + "io" + "io/ioutil" + "os" + "testing" + + _ "image/jpeg" // registers JPEG format for image.Decode + "image/png" // registers PNG format for image.Decode + + "github.com/stretchr/testify/require" +) + +const ( + goodPNG = "../../../testdata/image.png" + badPNG = "../../../testdata/image_bad_iccp.png" + strippedPNG = "../../../testdata/image_stripped_iccp.png" + jpg = "../../../testdata/image.jpg" +) + +func TestReadImageUnchanged(t *testing.T) { + testCases := []struct { + desc string + imagePath string + imageType string + }{ + { + desc: "image is not a PNG", + imagePath: jpg, + imageType: "jpeg", + }, + { + desc: "image is PNG without iCCP chunk", + imagePath: goodPNG, + imageType: "png", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + requireValidImage(t, pngReader(t, tc.imagePath), tc.imageType) + requireStreamUnchanged(t, pngReader(t, tc.imagePath), rawImageReader(t, tc.imagePath)) + }) + } +} + +func TestReadPNGWithBadICCPChunkDecodesAndReEncodesSuccessfully(t *testing.T) { + badPNGBytes, fmt, err := image.Decode(pngReader(t, badPNG)) + require.NoError(t, err) + require.Equal(t, "png", fmt) + + strippedPNGBytes, fmt, err := image.Decode(pngReader(t, strippedPNG)) + require.NoError(t, err) + require.Equal(t, "png", fmt) + + buf1 := new(bytes.Buffer) + buf2 := new(bytes.Buffer) + + require.NoError(t, png.Encode(buf1, badPNGBytes)) + require.NoError(t, png.Encode(buf2, strippedPNGBytes)) + + requireStreamUnchanged(t, buf1, buf2) +} + +func pngReader(t *testing.T, path string) io.Reader { + r, err := NewReader(rawImageReader(t, path)) + require.NoError(t, err) + return r +} + +func rawImageReader(t *testing.T, path string) io.Reader { + f, err := os.Open(path) + require.NoError(t, err) + return f +} + +func requireValidImage(t *testing.T, r io.Reader, expected string) { + _, fmt, err := image.Decode(r) + require.NoError(t, err) + require.Equal(t, expected, fmt) +} + +func requireStreamUnchanged(t *testing.T, actual io.Reader, expected io.Reader) { + actualBytes, err := ioutil.ReadAll(actual) + require.NoError(t, err) + expectedBytes, err := ioutil.ReadAll(expected) + require.NoError(t, err) + + table := crc64.MakeTable(crc64.ISO) + sumActual := crc64.Checksum(actualBytes, table) + sumExpected := crc64.Checksum(expectedBytes, table) + require.Equal(t, sumExpected, sumActual) +} |