Skip to content
Merged

Misc #42

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import (
"encoding"
"errors"
"fmt"
"io"
"math"
"runtime"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -471,12 +469,6 @@ func trimBytesNulls(b []byte) []byte {
return b[lo : hi+1]
}

func printStackTrace(w io.Writer) {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Fprintf(w, "%s", buf)
}

func typeAssertSlice[T any](ctx valueConverterContext, v any) ([]T, bool) {
vv, ok := v.([]T)
if ok {
Expand Down
109 changes: 59 additions & 50 deletions imagemeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"io"
"maps"
"math"
"os"
"strings"
"time"
)
Expand Down Expand Up @@ -51,49 +50,65 @@ const (
func Decode(opts Options) (err error) {
var base *baseStreamingDecoder

defer func() {
if r := recover(); r != nil {
if errp, ok := r.(error); ok {
if isInvalidFormatErrorCandidate(errp) {
err = newInvalidFormatError(errp)
} else {
err = errp
if err != errStop {
printStackTrace(os.Stderr)
}
}
} else {
err = fmt.Errorf("unknown panic: %v", r)
printStackTrace(os.Stderr)
}
errFinal := func(err2 error) error {
if err2 == nil {
return nil
}

if err == ErrStopWalking {
err = nil
return
if err2 == ErrStopWalking {
return nil
}

if err == errStop {
err = nil
if err2 == errStop {
return nil
}

if err == nil {
if err2 == nil {
if base != nil {
err = base.streamErr()
err2 = base.streamErr()
}
}

if err == nil {
return
if err2 == nil {
return nil
}

if err == io.EOF {
err = nil
return
if err2 == io.EOF {
return nil
}

if isInvalidFormatErrorCandidate(err) {
err = newInvalidFormatError(err)
if isInvalidFormatErrorCandidate(err2) {
err2 = newInvalidFormatError(err2)
}

return err2
}

defer func() {
err = errFinal(err)
}()

errFromRecover := func(r any) (err2 error) {
if r == nil {
return nil
}
if errp, ok := r.(error); ok {
if isInvalidFormatErrorCandidate(errp) {
err2 = newInvalidFormatError(errp)
} else {
err2 = errp
}
} else {
err2 = fmt.Errorf("unknown panic: %v", r)
}

return
}

defer func() {
err2 := errFromRecover(recover())
if err == nil {
err = err2
}
}()

Expand Down Expand Up @@ -195,32 +210,26 @@ func Decode(opts Options) (err error) {
dec = &imageDecoderPNG{baseStreamingDecoder: base}
}

if opts.Timeout > 0 {
decode := func() chan error {
errc := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
if errp, ok := r.(error); ok {
if errp != errStop {
printStackTrace(os.Stderr)
}
errc <- errp
} else {
errc <- fmt.Errorf("unknown panic: %v", r)
printStackTrace(os.Stderr)
}
err2 := errFromRecover(recover())
if err2 != nil {
errc <- err2
}
}()
select {
case <-time.After(opts.Timeout):
printStackTrace(os.Stderr)
errc <- fmt.Errorf("timed out after %s", opts.Timeout)
case errc <- dec.decode():
}
errc <- dec.decode()
}()
return errc
}

err = <-errc

if opts.Timeout > 0 {
select {
case <-time.After(opts.Timeout):
err = fmt.Errorf("timed out after %s", opts.Timeout)
case err = <-decode():
}
} else {
err = dec.decode()
}
Expand Down Expand Up @@ -277,7 +286,7 @@ type Options struct {
// Tag values larger than this will be skipped without notice.
// Note that this limit is not relevant for the XMP source.
// Default value is 10000.
LimitTagSize uint16
LimitTagSize uint32
}

// TagInfo contains information about a tag.
Expand Down
5 changes: 3 additions & 2 deletions imagemeta_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -67,9 +68,9 @@ func FuzzDecodeTIFF(f *testing.F) {

func fuzzDecodeBytes(t *testing.T, imageBytes []byte, f imagemeta.ImageFormat) error {
r := bytes.NewReader(imageBytes)
err := imagemeta.Decode(imagemeta.Options{R: r, ImageFormat: f, Sources: imagemeta.EXIF | imagemeta.IPTC | imagemeta.XMP, Timeout: 10 * time.Second})
err := imagemeta.Decode(imagemeta.Options{R: r, ImageFormat: f, Sources: imagemeta.EXIF | imagemeta.IPTC | imagemeta.XMP, Timeout: 600 * time.Millisecond})
if err != nil {
if !imagemeta.IsInvalidFormat(err) {
if !imagemeta.IsInvalidFormat(err) && !strings.Contains(err.Error(), "timed out") {
t.Fatalf("unknown error in Decode: %v %T", err, err)
}
}
Expand Down
70 changes: 52 additions & 18 deletions imagemeta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strconv"
"strings"
"testing"
"time"

"github.com/bep/imagemeta"
"github.com/rwcarlsen/goexif/exif"
Expand Down Expand Up @@ -61,7 +62,8 @@ func TestDecodeAllImageFormats(t *testing.T) {

func TestDecodeWebP(t *testing.T) {
c := qt.New(t)
tags := extractTags(t, "sunrise.webp", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP)
tags, err := extractTags(t, "sunrise.webp", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP)
c.Assert(err, qt.IsNil)

c.Assert(tags.EXIF()["Copyright"].Value, qt.Equals, "Bjørn Erik Pedersen")
c.Assert(tags.EXIF()["ApertureValue"].Value, eq, 5.6)
Expand All @@ -83,7 +85,8 @@ func TestDecodeJPEG(t *testing.T) {
return true
}

tags := extractTagsWithFilter(t, "sunrise.jpg", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP, shouldInclude)
tags, err := extractTagsWithFilter(t, "sunrise.jpg", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP, shouldInclude)
c.Assert(err, qt.IsNil)

c.Assert(tags.EXIF()["Copyright"].Value, qt.Equals, "Bjørn Erik Pedersen")
c.Assert(tags.EXIF()["ApertureValue"].Value, eq, 5.6)
Expand All @@ -99,7 +102,8 @@ func TestDecodePNG(t *testing.T) {
return true
}

tags := extractTagsWithFilter(t, "sunrise.png", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP, shouldInclude)
tags, err := extractTagsWithFilter(t, "sunrise.png", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP, shouldInclude)
c.Assert(err, qt.IsNil)

c.Assert(len(tags.EXIF()), qt.Equals, 61)
c.Assert(len(tags.IPTC()), qt.Equals, 14)
Expand All @@ -119,7 +123,8 @@ func TestThumbnailOffset(t *testing.T) {
}

offset := func(filename string) uint32 {
tags := extractTagsWithFilter(t, filename, imagemeta.EXIF, shouldHandle)
tags, err := extractTagsWithFilter(t, filename, imagemeta.EXIF, shouldHandle)
c.Assert(err, qt.IsNil)
return tags.EXIF()["ThumbnailOffset"].Value.(uint32)
}

Expand All @@ -132,7 +137,8 @@ func TestThumbnailOffset(t *testing.T) {
func TestDecodeTIFF(t *testing.T) {
c := qt.New(t)

tags := extractTags(t, "sunrise.tif", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP)
tags, err := extractTags(t, "sunrise.tif", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP)
c.Assert(err, qt.IsNil)

c.Assert(len(tags.EXIF()), qt.Equals, 76)
c.Assert(len(tags.XMP()), qt.Equals, 146)
Expand Down Expand Up @@ -307,7 +313,8 @@ func TestDecodeNamespace(t *testing.T) {
return true
}

tags := extractTagsWithFilter(t, "sunrise.jpg", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP, shouldInclude)
tags, err := extractTagsWithFilter(t, "sunrise.jpg", imagemeta.EXIF|imagemeta.IPTC|imagemeta.XMP, shouldInclude)
c.Assert(err, qt.IsNil)

c.Assert(tags.EXIF()["Artist"].Namespace, qt.Equals, "IFD0")
c.Assert(tags.EXIF()["GPSLatitude"].Namespace, qt.Equals, "IFD0/GPSInfoIFD")
Expand Down Expand Up @@ -376,10 +383,25 @@ func TestDecodeIPTCOrientationOnly(t *testing.T) {
c.Assert(len(tags.IPTC()), qt.Equals, 1)
}

func TestDecodeLargeExifTimeout(t *testing.T) {
c := qt.New(t)

withOpts := func(opts *imagemeta.Options) {
opts.Timeout = time.Duration(500 * time.Millisecond)

// Set the limits to something high to make sure we time out.
opts.LimitNumTags = 1000000
opts.LimitTagSize = 10000000
}
_, err := extractTags(t, "largeexif.png", imagemeta.EXIF, withOpts)
c.Assert(err, qt.ErrorMatches, "timed out after 500ms")
}

func TestDecodeXMPJPG(t *testing.T) {
c := qt.New(t)

tags := extractTags(t, "sunrise.jpg", imagemeta.XMP)
tags, err := extractTags(t, "sunrise.jpg", imagemeta.XMP)
c.Assert(err, qt.IsNil)

c.Assert(len(tags.EXIF()) == 0, qt.IsTrue)
c.Assert(len(tags.IPTC()) == 0, qt.IsTrue)
Expand Down Expand Up @@ -437,14 +459,16 @@ func TestGoldenTagCountXMP(t *testing.T) {
func TestLatLong(t *testing.T) {
c := qt.New(t)

tags := extractTags(t, "sunrise.jpg", imagemeta.EXIF)
tags, err := extractTags(t, "sunrise.jpg", imagemeta.EXIF)
c.Assert(err, qt.IsNil)

lat, long, err := tags.GetLatLong()
c.Assert(err, qt.IsNil)
c.Assert(lat, eq, float64(36.59744166))
c.Assert(long, eq, float64(-4.50846))

tags = extractTags(t, "goexif/geodegrees_as_string.jpg", imagemeta.EXIF)
tags, err = extractTags(t, "goexif/geodegrees_as_string.jpg", imagemeta.EXIF)
c.Assert(err, qt.IsNil)
lat, long, err = tags.GetLatLong()
c.Assert(err, qt.IsNil)
c.Assert(lat, eq, float64(52.013888888))
Expand All @@ -454,7 +478,8 @@ func TestLatLong(t *testing.T) {
func TestGetDateTime(t *testing.T) {
c := qt.New(t)

tags := extractTags(t, "sunrise.jpg", imagemeta.EXIF)
tags, err := extractTags(t, "sunrise.jpg", imagemeta.EXIF)
c.Assert(err, qt.IsNil)
d, err := tags.GetDateTime()
c.Assert(err, qt.IsNil)
c.Assert(d.Format("2006-01-02"), qt.Equals, "2017-10-27")
Expand Down Expand Up @@ -512,7 +537,8 @@ func assertGoldenInfoTagCount(t testing.TB, filename string, sources imagemeta.S
return true
}

tags := extractTagsWithFilter(t, filename, sources, shouldHandle)
tags, err := extractTagsWithFilter(t, filename, sources, shouldHandle)
c.Assert(err, qt.IsNil)
all := tags.All()

// Our XMP parsing is currently a little limited so be a little lenient with the assertions.
Expand Down Expand Up @@ -587,7 +613,8 @@ func assertGoldenInfoTagCount(t testing.TB, filename string, sources imagemeta.S

func compareWithExiftoolOutput(t testing.TB, filename string, sources imagemeta.Source) {
c := qt.New(t)
tags := extractTags(t, filename, sources)
tags, err := extractTags(t, filename, sources)
c.Assert(err, qt.IsNil)
all := tags.All()
tagsGolden := readGoldenInfo(t, filename)

Expand Down Expand Up @@ -731,15 +758,17 @@ func extToFormat(ext string) imagemeta.ImageFormat {
}
}

func extractTags(t testing.TB, filename string, sources imagemeta.Source) imagemeta.Tags {
func extractTags(t testing.TB, filename string, sources imagemeta.Source, opts ...withOptions) (imagemeta.Tags, error) {
shouldHandle := func(ti imagemeta.TagInfo) bool {
// Drop the thumbnail tags.
return ti.Namespace != "IFD1"
}
return extractTagsWithFilter(t, filename, sources, shouldHandle)
return extractTagsWithFilter(t, filename, sources, shouldHandle, opts...)
}

func extractTagsWithFilter(t testing.TB, filename string, sources imagemeta.Source, shouldHandle func(ti imagemeta.TagInfo) bool) imagemeta.Tags {
type withOptions func(opts *imagemeta.Options)

func extractTagsWithFilter(t testing.TB, filename string, sources imagemeta.Source, shouldHandle func(ti imagemeta.TagInfo) bool, opts ...withOptions) (imagemeta.Tags, error) {
t.Helper()
if !filepath.IsAbs(filename) {
filename = filepath.Join("testdata", "images", filename)
Expand Down Expand Up @@ -770,9 +799,14 @@ func extractTagsWithFilter(t testing.TB, filename string, sources imagemeta.Sour
panic(errors.New(s))
}

err = imagemeta.Decode(imagemeta.Options{R: f, ImageFormat: imageFormat, ShouldHandleTag: shouldHandle, HandleTag: handleTag, Warnf: warnf, Sources: sources})
imgOpts := imagemeta.Options{R: f, ImageFormat: imageFormat, ShouldHandleTag: shouldHandle, HandleTag: handleTag, Warnf: warnf, Sources: sources}
for _, opt := range opts {
opt(&imgOpts)
}

err = imagemeta.Decode(imgOpts)
if err != nil {
t.Fatal(fmt.Errorf("failed to decode %q: %w", filename, err))
return tags, err
}

// See https://github.com/gohugoio/hugo/issues/12741 and https://github.com/golang/go/issues/59627
Expand All @@ -788,7 +822,7 @@ func extractTagsWithFilter(t testing.TB, filename string, sources imagemeta.Sour
t.Fatal(fmt.Errorf("failed to marshal tags in %q to JSON: %w", filename, err))
}

return tags
return tags, nil
}

func readGoldenInfo(t testing.TB, filename string) goldenFileInfo {
Expand Down
Loading
Loading