package test import ( domainImage "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/imaging" "RenewCMS/internal/infrastructure/useCases" "bytes" "crypto/sha256" "encoding/binary" "encoding/hex" "errors" "hash/crc32" "image" "image/color" "image/jpeg" "image/png" "strings" "testing" "github.com/gen2brain/webp" xwebp "golang.org/x/image/webp" ) const metadataSecret = "GPS-48.8584N-2.2945E" func metadataTestImage() *image.NRGBA { img := image.NewNRGBA(image.Rect(0, 0, 8, 6)) for y := 0; y < 6; y++ { for x := 0; x < 8; x++ { img.Set(x, y, color.NRGBA{R: uint8(x * 30), G: uint8(y * 40), B: 90, A: 255}) } } return img } type exifByteOrder interface { binary.ByteOrder binary.AppendByteOrder } func exifTIFF(order exifByteOrder, orientation uint16) []byte { marker := []byte("II") if order.String() == binary.BigEndian.String() { marker = []byte("MM") } text := append([]byte(metadataSecret), 0) if len(text)%2 == 1 { text = append(text, 0) } ifd := uint32(8 + len(text)) tiff := append(marker, 0, 0) order.PutUint16(tiff[2:], 42) tiff = order.AppendUint32(tiff, ifd) tiff = append(tiff, text...) tiff = order.AppendUint16(tiff, 2) tiff = order.AppendUint16(tiff, 0x010F) tiff = order.AppendUint16(tiff, 2) tiff = order.AppendUint32(tiff, uint32(len(metadataSecret)+1)) tiff = order.AppendUint32(tiff, 8) tiff = order.AppendUint16(tiff, 0x0112) tiff = order.AppendUint16(tiff, 3) tiff = order.AppendUint32(tiff, 1) tiff = order.AppendUint16(tiff, orientation) tiff = order.AppendUint16(tiff, 0) return order.AppendUint32(tiff, 0) } func jpegSegmentBytes(marker byte, payload []byte) []byte { segment := []byte{0xFF, marker} segment = binary.BigEndian.AppendUint16(segment, uint16(len(payload)+2)) return append(segment, payload...) } func encodedJPEG(t *testing.T) []byte { t.Helper() var buf bytes.Buffer if err := jpeg.Encode(&buf, metadataTestImage(), &jpeg.Options{Quality: 90}); err != nil { t.Fatal(err) } return buf.Bytes() } func jpegWithMetadata(t *testing.T, orientation uint16, extra ...[]byte) []byte { t.Helper() plain := encodedJPEG(t) out := append([]byte{}, plain[:2]...) for _, segment := range extra { out = append(out, segment...) } out = append(out, jpegSegmentBytes(0xE1, append([]byte("Exif\x00\x00"), exifTIFF(binary.LittleEndian, orientation)...))...) out = append(out, jpegSegmentBytes(0xE1, []byte("http://ns.adobe.com/xap/1.0/\x00"+metadataSecret+""))...) out = append(out, jpegSegmentBytes(0xED, []byte("Photoshop 3.0\x008BIM"+metadataSecret))...) out = append(out, jpegSegmentBytes(0xFE, []byte("comment "+metadataSecret))...) out = append(out, jpegSegmentBytes(0xE2, []byte("MPF\x00"+metadataSecret))...) out = append(out, jpegSegmentBytes(0xE2, []byte("ICC_PROFILE\x00\x01\x01profile-bytes"))...) out = append(out, plain[2:]...) return append(out, []byte("\xFF\xD8trailing image "+metadataSecret+"\xFF\xD9")...) } func pngChunkBytes(kind string, payload []byte) []byte { chunk := binary.BigEndian.AppendUint32(nil, uint32(len(payload))) chunk = append(chunk, kind...) chunk = append(chunk, payload...) return binary.BigEndian.AppendUint32(chunk, crc32.ChecksumIEEE(chunk[4:])) } func encodedPNG(t *testing.T) []byte { t.Helper() var buf bytes.Buffer if err := png.Encode(&buf, metadataTestImage()); err != nil { t.Fatal(err) } return buf.Bytes() } func pngWithMetadata(t *testing.T, orientation uint16) []byte { t.Helper() plain := encodedPNG(t) afterIHDR := 8 + 8 + 13 + 4 out := append([]byte{}, plain[:afterIHDR]...) out = append(out, pngChunkBytes("eXIf", exifTIFF(binary.BigEndian, orientation))...) out = append(out, pngChunkBytes("tEXt", []byte("Comment\x00"+metadataSecret))...) out = append(out, pngChunkBytes("iTXt", []byte("XML:com.adobe.xmp\x00\x00\x00\x00\x00"+metadataSecret))...) out = append(out, pngChunkBytes("zTXt", []byte("Author\x00\x00"+metadataSecret))...) out = append(out, pngChunkBytes("tIME", []byte{0x07, 0xEA, 10, 22, 9, 0, 0})...) out = append(out, plain[afterIHDR:]...) return append(out, []byte("trailing "+metadataSecret)...) } func riffChunkBytes(kind string, payload []byte) []byte { chunk := append([]byte(kind), binary.LittleEndian.AppendUint32(nil, uint32(len(payload)))...) chunk = append(chunk, payload...) if len(payload)%2 == 1 { chunk = append(chunk, 0) } return chunk } func webpWithMetadata(t *testing.T, orientation uint16) []byte { t.Helper() var buf bytes.Buffer if err := webp.Encode(&buf, metadataTestImage(), webp.Options{Lossless: true}); err != nil { t.Fatal(err) } simple := buf.Bytes() vp8x := []byte{0x08 | 0x04, 0, 0, 0} vp8x = append(vp8x, 7, 0, 0, 5, 0, 0) body := []byte("WEBP") body = append(body, riffChunkBytes("VP8X", vp8x)...) body = append(body, simple[12:]...) body = append(body, riffChunkBytes("EXIF", exifTIFF(binary.LittleEndian, orientation))...) body = append(body, riffChunkBytes("XMP ", []byte(""+metadataSecret+"!"))...) out := append([]byte("RIFF"), binary.LittleEndian.AppendUint32(nil, uint32(len(body)))...) out = append(out, body...) return append(out, []byte("trailing "+metadataSecret)...) } func assertSamePixels(t *testing.T, want image.Image, got image.Image) { t.Helper() if want.Bounds() != got.Bounds() { t.Fatalf("bounds changed: %v vs %v", want.Bounds(), got.Bounds()) } for y := want.Bounds().Min.Y; y < want.Bounds().Max.Y; y++ { for x := want.Bounds().Min.X; x < want.Bounds().Max.X; x++ { if want.At(x, y) != got.At(x, y) { t.Fatalf("pixel (%d,%d) changed: %v vs %v", x, y, want.At(x, y), got.At(x, y)) } } } } func assertNoSecret(t *testing.T, data []byte) { t.Helper() if bytes.Contains(data, []byte(metadataSecret)) { t.Errorf("the metadata secret survived stripping") } } func orientationOnlyEntry(orientation uint16) []byte { entry := []byte{0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01} return binary.BigEndian.AppendUint16(entry, orientation) } func TestMetadataStripperJPEG(t *testing.T) { stripper := imaging.NewMetadataStripper() t.Run("Drops EXIF, XMP, IPTC, comments, MPF and trailing images but keeps the pixels and the ICC profile", func(t *testing.T) { original := jpegWithMetadata(t, 1) stripped, err := stripper.Strip("image/jpeg", original) if err != nil { t.Fatal(err) } assertNoSecret(t, stripped) if !bytes.Contains(stripped, []byte("ICC_PROFILE\x00")) { t.Error("the ICC profile must be kept") } if bytes.Contains(stripped, []byte("Exif\x00\x00")) { t.Error("an upright image needs no EXIF at all") } if !bytes.HasSuffix(stripped, []byte{0xFF, 0xD9}) || bytes.Count(stripped, []byte{0xFF, 0xD8}) != 1 { t.Error("the output must end at the primary image's EOI") } want, _ := jpeg.Decode(bytes.NewReader(encodedJPEG(t))) got, err := jpeg.Decode(bytes.NewReader(stripped)) if err != nil { t.Fatalf("the stripped JPEG does not decode: %v", err) } assertSamePixels(t, want, got) }) t.Run("Keeps only the orientation of a rotated photo, right after SOI", func(t *testing.T) { stripped, err := stripper.Strip("image/jpeg", jpegWithMetadata(t, 6)) if err != nil { t.Fatal(err) } assertNoSecret(t, stripped) if !bytes.Equal(stripped[2:4], []byte{0xFF, 0xE1}) || !bytes.Contains(stripped[:64], orientationOnlyEntry(6)) { t.Errorf("expected an orientation-only APP1 after SOI, got % x", stripped[:48]) } if _, err := jpeg.Decode(bytes.NewReader(stripped)); err != nil { t.Errorf("the stripped JPEG does not decode: %v", err) } }) t.Run("Puts the orientation after a leading JFIF header", func(t *testing.T) { jfif := jpegSegmentBytes(0xE0, []byte("JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00")) stripped, err := stripper.Strip("image/jpeg", jpegWithMetadata(t, 8, jfif)) if err != nil { t.Fatal(err) } next := 2 + len(jfif) if !bytes.Equal(stripped[2:next], jfif) || !bytes.Equal(stripped[next:next+2], []byte{0xFF, 0xE1}) { t.Errorf("expected JFIF then the orientation APP1, got % x", stripped[:next+4]) } }) t.Run("A JPEG cut off inside its scan keeps what it has", func(t *testing.T) { plain := encodedJPEG(t) truncated := plain[:len(plain)-20] stripped, err := stripper.Strip("image/jpeg", truncated) if err != nil || !bytes.Equal(stripped, truncated) { t.Errorf("expected the truncated scan kept as is, got %d bytes, %v", len(stripped), err) } }) malformed := map[string][]byte{ "not a JPEG": []byte("GIF89a"), "a segment longer than the file": {0xFF, 0xD8, 0xFF, 0xE1, 0x10, 0x00, 'E'}, "a segment length below two": {0xFF, 0xD8, 0xFF, 0xE1, 0x00, 0x01}, "a missing marker": {0xFF, 0xD8, 0x00, 0x00}, "a header that stops early": {0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x04, 0, 0}, } for name, data := range malformed { t.Run("Refuses "+name, func(t *testing.T) { if _, err := stripper.Strip("image/jpeg", data); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) } } func TestMetadataStripperPNG(t *testing.T) { stripper := imaging.NewMetadataStripper() t.Run("Drops text, time and EXIF chunks and trailing data but keeps the pixels", func(t *testing.T) { stripped, err := stripper.Strip("image/png", pngWithMetadata(t, 1)) if err != nil { t.Fatal(err) } assertNoSecret(t, stripped) for _, kind := range []string{"eXIf", "tEXt", "iTXt", "zTXt", "tIME"} { if bytes.Contains(stripped, []byte(kind)) { t.Errorf("%s survived", kind) } } if !bytes.Equal(stripped, encodedPNG(t)) { t.Error("expected exactly the plain PNG back") } }) t.Run("Keeps only the orientation in an eXIf chunk right after IHDR", func(t *testing.T) { stripped, err := stripper.Strip("image/png", pngWithMetadata(t, 3)) if err != nil { t.Fatal(err) } assertNoSecret(t, stripped) afterIHDR := 8 + 8 + 13 + 4 if string(stripped[afterIHDR+4:afterIHDR+8]) != "eXIf" || !bytes.Contains(stripped, orientationOnlyEntry(3)) { t.Errorf("expected an orientation-only eXIf after IHDR") } want, _ := png.Decode(bytes.NewReader(encodedPNG(t))) got, err := png.Decode(bytes.NewReader(stripped)) if err != nil { t.Fatalf("the stripped PNG does not decode: %v", err) } assertSamePixels(t, want, got) }) plain := func(t *testing.T) []byte { return encodedPNG(t) } malformed := map[string]func(t *testing.T) []byte{ "not a PNG": func(*testing.T) []byte { return []byte("GIF89a") }, "a PNG without IEND": func(t *testing.T) []byte { p := plain(t); return p[:len(p)-12] }, "a chunk longer than the file": func(t *testing.T) []byte { p := plain(t); binary.BigEndian.PutUint32(p[8:], 1<<30); return p }, "a chunk header that stops early": func(t *testing.T) []byte { return append(plain(t)[:8], 0, 0, 0) }, } for name, build := range malformed { t.Run("Refuses "+name, func(t *testing.T) { if _, err := stripper.Strip("image/png", build(t)); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) } } func TestMetadataStripperWebP(t *testing.T) { stripper := imaging.NewMetadataStripper() t.Run("Drops XMP and EXIF, clears their flags and keeps the pixels", func(t *testing.T) { original := webpWithMetadata(t, 1) stripped, err := stripper.Strip("image/webp", original) if err != nil { t.Fatal(err) } assertNoSecret(t, stripped) if bytes.Contains(stripped, []byte("EXIF")) || bytes.Contains(stripped, []byte("XMP ")) { t.Error("metadata chunks survived") } if flags := stripped[20]; flags&0x0C != 0 { t.Errorf("expected the EXIF and XMP flags cleared, got %#x", flags) } if size := binary.LittleEndian.Uint32(stripped[4:]); int(size) != len(stripped)-8 { t.Errorf("RIFF size %d does not match %d bytes", size, len(stripped)-8) } want, _ := xwebp.Decode(bytes.NewReader(original)) got, err := xwebp.Decode(bytes.NewReader(stripped)) if err != nil { t.Fatalf("the stripped WebP does not decode: %v", err) } assertSamePixels(t, want, got) }) t.Run("Keeps only the orientation of a rotated image", func(t *testing.T) { stripped, err := stripper.Strip("image/webp", webpWithMetadata(t, 8)) if err != nil { t.Fatal(err) } assertNoSecret(t, stripped) if flags := stripped[20]; flags&0x0C != 0x08 { t.Errorf("expected only the EXIF flag set, got %#x", flags) } if !bytes.Contains(stripped, orientationOnlyEntry(8)) { t.Error("expected an orientation-only EXIF chunk") } if _, err := xwebp.Decode(bytes.NewReader(stripped)); err != nil { t.Errorf("the stripped WebP does not decode: %v", err) } }) t.Run("A simple WebP without extended header comes back unchanged", func(t *testing.T) { var buf bytes.Buffer if err := webp.Encode(&buf, metadataTestImage(), webp.Options{Lossless: true}); err != nil { t.Fatal(err) } stripped, err := stripper.Strip("image/webp", buf.Bytes()) if err != nil || !bytes.Equal(stripped, buf.Bytes()) { t.Errorf("expected the same bytes back, got %v", err) } }) malformed := map[string][]byte{ "not a WebP": []byte("RIFF\x04\x00\x00\x00WAVE"), "a RIFF size beyond the file": []byte("RIFF\xff\x00\x00\x00WEBP"), "a chunk longer than the file": append([]byte("RIFF\x10\x00\x00\x00WEBPVP8L"), 0xff, 0, 0, 0), "a container with no chunks": []byte("RIFF\x04\x00\x00\x00WEBP"), "an empty extended header": append([]byte("RIFF\x0c\x00\x00\x00WEBPVP8X"), 0, 0, 0, 0), "a chunk header that stops early": []byte("RIFF\x08\x00\x00\x00WEBPVP8X"), } for name, data := range malformed { t.Run("Refuses "+name, func(t *testing.T) { if _, err := stripper.Strip("image/webp", data); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) } } func TestMetadataStripperLeavesSVGAlone(t *testing.T) { svg := []byte(`keep`) stripped, err := imaging.NewMetadataStripper().Strip("image/svg+xml", svg) if err != nil || !bytes.Equal(stripped, svg) { t.Errorf("expected the SVG untouched, got %v", err) } } func TestUploadStoresTheStrippedImage(t *testing.T) { repo := newMockImageRepo() store := newMockImageStorage() uc := useCases.NewCreateImageUseCase(repo, store, discardRecorder, imaging.NewMetadataStripper()) original := jpegWithMetadata(t, 6) created, err := uc.CreateImage(testCtx, domainImage.ImageInput{ Content: bytes.NewReader(original), Filename: "holiday.jpg", Size: int64(len(original)), ContentType: "image/jpeg", }) if err != nil { t.Fatal(err) } stored := store.lastContent assertNoSecret(t, stored) if created.Size != int64(len(stored)) { t.Errorf("expected the recorded size %d to match the stored %d bytes", created.Size, len(stored)) } sum := sha256.Sum256(stored) if created.SHA256 != hex.EncodeToString(sum[:]) { t.Error("expected the checksum of the stored bytes") } if created.Width != 8 || created.Height != 6 { t.Errorf("expected 8x6, got %dx%d", created.Width, created.Height) } } func TestUploadRefusesAnImageWhoseContainerIsBroken(t *testing.T) { store := newMockImageStorage() uc := useCases.NewCreateImageUseCase(newMockImageRepo(), store, discardRecorder, imaging.NewMetadataStripper()) broken := encodedPNG(t) broken = broken[:len(broken)-12] _, err := uc.CreateImage(testCtx, domainImage.ImageInput{Content: bytes.NewReader(broken), Filename: "broken.png", ContentType: "image/png"}) if !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } if store.putCalls != 0 { t.Error("nothing may be stored") } } func hexBytes(t *testing.T, value string) []byte { t.Helper() decoded, err := hex.DecodeString(strings.ReplaceAll(value, " ", "")) if err != nil { t.Fatal(err) } return decoded } func jpegWithExif(tiff []byte) []byte { out := []byte{0xFF, 0xD8} out = append(out, jpegSegmentBytes(0xE1, append([]byte("Exif\x00\x00"), tiff...))...) return append(out, 0xFF, 0xD9) } func TestMetadataStripperJPEGStructure(t *testing.T) { stripper := imaging.NewMetadataStripper() kept := map[string]string{ "the smallest JPEG, SOI then EOI": "FFD8 FFD9", "standalone TEM and RST markers between segments": "FFD8 FF01 FFD0 FFD7 FFD9", "restart markers and stuffed bytes inside a scan": "FFD8 FFDA 0002 11 FF00 22 FFD0 33 FFD7 44 FFD9", "a scan cut off right after a 0xFF byte": "FFD8 FFDA 0002 11 22 FF", "a scan cut off right after a stuffed byte": "FFD8 FFDA 0002 11 FF00", "an Adobe APP14 segment": "FFD8 FFEE 0007 41646F6265 FFD9", "an ICC profile in APP2": "FFD8 FFE2 000E 4943435F50524F46494C4500 FFD9", "a quantisation table, which is not metadata at all": "FFD8 FFDB 0003 00 FFD9", } for name, value := range kept { t.Run("Keeps "+name, func(t *testing.T) { data := hexBytes(t, value) stripped, err := stripper.Strip("image/jpeg", data) if err != nil || !bytes.Equal(stripped, data) { t.Errorf("expected the bytes back unchanged, got % X, %v", stripped, err) } }) } removed := map[string]string{ "fill bytes before a marker": "FFD8 FFFFFF FFD9", "an empty comment": "FFD8 FFFE 0002 FFD9", "an APP3 segment": "FFD8 FFE3 0003 00 FFD9", "an APP12 segment": "FFD8 FFEC 0003 00 FFD9", "an APP15 segment": "FFD8 FFEF 0003 00 FFD9", "a FlashPix APP2 segment": "FFD8 FFE2 0006 46505852 FFD9", "an XMP APP1 segment": "FFD8 FFE1 0006 68747470 FFD9", } for name, value := range removed { t.Run("Drops "+name, func(t *testing.T) { stripped, err := stripper.Strip("image/jpeg", hexBytes(t, value)) if err != nil || !bytes.Equal(stripped, []byte{0xFF, 0xD8, 0xFF, 0xD9}) { t.Errorf("expected only SOI and EOI, got % X, %v", stripped, err) } }) } refused := map[string]string{ "only three bytes": "FFD8 FF", "fill bytes up to the end": "FFD8 FFFF", "a segment length cut in half": "FFD8 FFE1 00", "a segment whose length runs past EOF": "FFD8 FFE1 0004 00", "no EOI after the header": "FFD8 FFDB 0003 00", } for name, value := range refused { t.Run("Refuses "+name, func(t *testing.T) { if _, err := stripper.Strip("image/jpeg", hexBytes(t, value)); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) } } func TestMetadataStripperReadsOnlyAWellFormedOrientation(t *testing.T) { stripper := imaging.NewMetadataStripper() valid := exifTIFF(binary.BigEndian, 8) ifd := int(binary.BigEndian.Uint32(valid[4:])) withValue := func(value uint16) []byte { tiff := bytes.Clone(valid) binary.BigEndian.PutUint16(tiff[ifd+2+12+8:], value) return tiff } withType := func(kind uint16) []byte { tiff := bytes.Clone(valid) binary.BigEndian.PutUint16(tiff[ifd+2+12+2:], kind) return tiff } withIFD := func(offset uint32) []byte { tiff := bytes.Clone(valid) binary.BigEndian.PutUint32(tiff[4:], offset) return tiff } withCount := func(count uint16) []byte { tiff := bytes.Clone(valid) binary.BigEndian.PutUint16(tiff[ifd:], count) return tiff[:ifd+2+12] } cases := []struct { name string tiff []byte want uint16 }{ {"a valid big-endian orientation", valid, 8}, {"a valid little-endian orientation", exifTIFF(binary.LittleEndian, 5), 5}, {"the smallest orientation that needs rotating", withValue(2), 2}, {"an orientation of 0", withValue(0), 0}, {"an orientation of 9", withValue(9), 0}, {"an orientation stored as a LONG", withType(4), 0}, {"a TIFF shorter than its header", valid[:7], 0}, {"an unknown byte order", append([]byte("XX"), valid[2:]...), 0}, {"a wrong magic number", append([]byte("MM\x00\x2B"), valid[4:]...), 0}, {"an IFD offset inside the header", withIFD(4), 0}, {"an IFD offset on the last byte", withIFD(uint32(len(valid) - 1)), 0}, {"an IFD offset two bytes before the end", withIFD(uint32(len(valid) - 2)), 0}, {"an entry count larger than the entries present", withCount(3), 0}, {"a TIFF cut inside the orientation entry", valid[:ifd+2+12+11], 0}, {"a TIFF that ends right after the orientation entry", valid[:ifd+2+24], 8}, {"an IFD that starts right after the header", hexBytes(t, "4D4D002A 00000008 0001 0112 0003 00000001 0003 0000 00000000"), 3}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { stripped, err := stripper.Strip("image/jpeg", jpegWithExif(tc.tiff)) if err != nil { t.Fatal(err) } hasOrientation := bytes.Contains(stripped, []byte("Exif\x00\x00")) if tc.want == 0 && hasOrientation { t.Errorf("expected no orientation kept, got % X", stripped) } if tc.want != 0 && !bytes.Contains(stripped, orientationOnlyEntry(tc.want)) { t.Errorf("expected orientation %d kept, got % X", tc.want, stripped) } }) } } func TestMetadataStripperPNGStructure(t *testing.T) { stripper := imaging.NewMetadataStripper() plain := encodedPNG(t) iend := plain[len(plain)-12:] body := plain[:len(plain)-12] t.Run("Keeps an empty ancillary chunk it does not know", func(t *testing.T) { data := append(append(bytes.Clone(body), pngChunkBytes("abCd", nil)...), iend...) stripped, err := stripper.Strip("image/png", data) if err != nil || !bytes.Equal(stripped, data) { t.Errorf("expected the bytes back unchanged, got %v", err) } }) t.Run("Refuses a chunk one byte longer than the file", func(t *testing.T) { data := bytes.Clone(plain) binary.BigEndian.PutUint32(data[len(data)-12:], 1) if _, err := stripper.Strip("image/png", data); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) t.Run("Refuses an IEND missing its last CRC byte", func(t *testing.T) { if _, err := stripper.Strip("image/png", plain[:len(plain)-1]); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) t.Run("Puts a kept orientation right after IHDR even when IHDR is followed by other chunks", func(t *testing.T) { afterIHDR := 8 + 8 + 13 + 4 data := append(bytes.Clone(plain[:afterIHDR]), pngChunkBytes("gAMA", []byte{0, 0, 0xB1, 0x8F})...) data = append(data, pngChunkBytes("eXIf", exifTIFF(binary.LittleEndian, 6))...) data = append(data, plain[afterIHDR:]...) stripped, err := stripper.Strip("image/png", data) if err != nil { t.Fatal(err) } if string(stripped[afterIHDR+4:afterIHDR+8]) != "eXIf" || string(stripped[afterIHDR+12+26+4:afterIHDR+12+26+8]) != "gAMA" { t.Errorf("expected eXIf then gAMA after IHDR") } if _, err := png.Decode(bytes.NewReader(stripped)); err != nil { t.Errorf("the stripped PNG does not decode: %v", err) } }) t.Run("Refuses a rotated PNG with no IHDR to put the orientation after", func(t *testing.T) { data := append(bytes.Clone(pngSignatureBytes), pngChunkBytes("eXIf", exifTIFF(binary.BigEndian, 6))...) data = append(data, pngChunkBytes("IEND", nil)...) if _, err := stripper.Strip("image/png", data); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) } var pngSignatureBytes = []byte("\x89PNG\r\n\x1a\n") func riffChunkKinds(t *testing.T, data []byte) []string { t.Helper() if int(binary.LittleEndian.Uint32(data[4:]))+8 != len(data) { t.Fatalf("RIFF size %d does not match %d bytes", binary.LittleEndian.Uint32(data[4:]), len(data)-8) } var kinds []string for pos := 12; pos < len(data); { size := int(binary.LittleEndian.Uint32(data[pos+4:])) kinds = append(kinds, string(data[pos:pos+4])) pos += 8 + size + size%2 if pos > len(data) { t.Fatalf("chunk %s runs past the end", kinds[len(kinds)-1]) } } return kinds } func TestMetadataStripperWebPStructure(t *testing.T) { stripper := imaging.NewMetadataStripper() t.Run("The rotated output is a well-formed chain of chunks", func(t *testing.T) { stripped, err := stripper.Strip("image/webp", webpWithMetadata(t, 6)) if err != nil { t.Fatal(err) } if got := strings.Join(riffChunkKinds(t, stripped), ","); got != "VP8X,VP8L,EXIF" { t.Errorf("expected VP8X,VP8L,EXIF, got %s", got) } }) t.Run("An upright output keeps only the image chunks", func(t *testing.T) { stripped, err := stripper.Strip("image/webp", webpWithMetadata(t, 1)) if err != nil { t.Fatal(err) } if got := strings.Join(riffChunkKinds(t, stripped), ","); got != "VP8X,VP8L" { t.Errorf("expected VP8X,VP8L, got %s", got) } }) t.Run("Keeps an empty chunk it does not know at the very end", func(t *testing.T) { original := webpWithMetadata(t, 1) declared := int(binary.LittleEndian.Uint32(original[4:])) + 8 data := append(bytes.Clone(original[:declared]), riffChunkBytes("ABCD", nil)...) binary.LittleEndian.PutUint32(data[4:], uint32(len(data)-8)) stripped, err := stripper.Strip("image/webp", data) if err != nil { t.Fatal(err) } if got := strings.Join(riffChunkKinds(t, stripped), ","); got != "VP8X,VP8L,ABCD" { t.Errorf("expected VP8X,VP8L,ABCD, got %s", got) } }) t.Run("Refuses an odd-sized chunk missing its padding byte", func(t *testing.T) { original := webpWithMetadata(t, 1) declared := int(binary.LittleEndian.Uint32(original[4:])) + 8 data := append(bytes.Clone(original[:declared]), "ABCD\x01\x00\x00\x00X"...) binary.LittleEndian.PutUint32(data[4:], uint32(len(data)-8)) if _, err := stripper.Strip("image/webp", data); !errors.Is(err, domainImage.ErrImageUnreadable) { t.Errorf("expected ErrImageUnreadable, got %v", err) } }) }