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(``)
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)
}
})
}