package analysis import ( "encoding/binary" "errors" "math" "strings" "time" ) // This file implements a deliberately small EXIF reader. Only the fields the // application actually uses are decoded: // // Make, Model IFD0, tags 0x010F / 0x0110 // DateTimeOriginal Exif SubIFD, tag 0x9003 // GPSLatitude / GPSLongitude GPS SubIFD, tags 0x0002 / 0x0004 // // EXIF is a TIFF structure embedded in a file, so parsing it means walking an // image file directory. Every offset read from the data is treated as untrusted // and bounds-checked, because the input is a user upload. // errNoEXIF is returned when the payload carries no EXIF block at all. That is // the normal case for pictures exported from messaging apps and screenshots. var errNoEXIF = errors.New("no EXIF block found") // exifHeaderLen is the length of the "Exif\x00\x00" identifier that introduces // the APP1 payload. const exifHeaderLen = 6 // searchWindow bounds how far into the file the EXIF identifier is searched. // APP1 comes near the front of a JPEG; a small window avoids scanning a // multi-megabyte payload. const searchWindow = 128 << 10 // TIFF field types from the TIFF 6.0 specification. const ( tiffByte = 1 tiffASCII = 2 tiffShort = 3 tiffLong = 4 tiffRational = 5 tiffUndefined = 7 ) type tiffTag uint16 const ( tagMake tiffTag = 0x010F tagModel tiffTag = 0x0110 tagExifIFDPointer tiffTag = 0x8769 tagGPSIFDPointer tiffTag = 0x8825 tagDateTimeOriginal tiffTag = 0x9003 tagGPSLatitudeRef tiffTag = 0x0001 tagGPSLatitude tiffTag = 0x0002 tagGPSLongitudeRef tiffTag = 0x0003 tagGPSLongitude tiffTag = 0x0004 ) // exifData holds the decoded fields. The zero value means "not present". type exifData struct { Make string Model string // TakenAt is the capture time. EXIF records no time zone, so it is // interpreted as UTC. TakenAt *time.Time // Latitude and Longitude are signed decimal degrees. Latitude float64 Longitude float64 HasGPS bool } // readEXIF extracts EXIF from a JPEG or PNG payload. A payload without EXIF // yields errNoEXIF, which callers treat as a normal outcome. func readEXIF(data []byte) (exifData, error) { var out exifData block, err := findExifBlock(data) if err != nil { return out, err } if err := parseTIFF(block, &out); err != nil { return exifData{}, err } return out, nil } // findExifBlock locates the TIFF structure inside the payload. JPEG stores it // in an APP1 segment and PNG in an "eXIf" chunk; both are preceded by the // "Exif\x00\x00" identifier in practice, so scanning for that identifier keeps // the reader format-agnostic. func findExifBlock(data []byte) ([]byte, error) { window := data if len(window) > searchWindow { window = window[:searchWindow] } const marker = "Exif\x00\x00" for i := 0; i+exifHeaderLen <= len(window); i++ { if string(window[i:i+exifHeaderLen]) != marker { continue } start := i + exifHeaderLen if start >= len(data) { return nil, errNoEXIF } return data[start:], nil } return nil, errNoEXIF } // parseTIFF walks the directory structure starting at the beginning of block. func parseTIFF(block []byte, out *exifData) error { if len(block) < 8 { return errors.New("EXIF: TIFF header is truncated") } var order binary.ByteOrder switch { case block[0] == 'I' && block[1] == 'I': order = binary.LittleEndian case block[0] == 'M' && block[1] == 'M': order = binary.BigEndian default: return errors.New("EXIF: unknown byte order marker") } if magic := order.Uint16(block[2:4]); magic != 42 { return errors.New("EXIF: bad TIFF magic number") } ifd0 := order.Uint32(block[4:8]) entries, next, err := readIFD(block, order, ifd0) if err != nil { return err } for _, e := range entries { switch e.tag { case tagMake: if s, ok := e.asASCII(block, order); ok { out.Make = s } case tagModel: if s, ok := e.asASCII(block, order); ok { out.Model = s } case tagExifIFDPointer: if sub, _, err := readIFD(block, order, uint32(e.firstValue(order))); err == nil { readExifSubIFD(block, order, sub, out) } case tagGPSIFDPointer: if sub, _, err := readIFD(block, order, uint32(e.firstValue(order))); err == nil { readGPSIFD(block, order, sub, out) } } } _ = next return nil } // entry is one directory entry. type entry struct { tag tiffTag typ uint16 count uint32 // inline is the 4-byte value field as read, used when the payload fits. inline [4]byte } func readExifSubIFD(block []byte, order binary.ByteOrder, entries []entry, out *exifData) { for _, e := range entries { if e.tag != tagDateTimeOriginal { continue } if s, ok := e.asASCII(block, order); ok { if t, err := time.Parse(exifTimeLayout, s); err == nil { utc := t.UTC() out.TakenAt = &utc } } } } func readGPSIFD(block []byte, order binary.ByteOrder, entries []entry, out *exifData) { var ( lat, lon float64 latOK, lonOK bool latRef, lonRef string ) for _, e := range entries { switch e.tag { case tagGPSLatitudeRef: if s, ok := e.asASCII(block, order); ok { latRef = s } case tagGPSLongitudeRef: if s, ok := e.asASCII(block, order); ok { lonRef = s } case tagGPSLatitude: if v, ok := e.asRationals(block, order); ok && len(v) >= 3 { lat, latOK = dmsToDecimal(v[0], v[1], v[2]) } case tagGPSLongitude: if v, ok := e.asRationals(block, order); ok && len(v) >= 3 { lon, lonOK = dmsToDecimal(v[0], v[1], v[2]) } } } if latOK && lonOK { // Southern latitudes and western longitudes are stored as magnitudes // with a separate hemisphere reference. if strings.EqualFold(latRef, "S") { lat = -lat } if strings.EqualFold(lonRef, "W") { lon = -lon } if lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180 { out.Latitude = lat out.Longitude = lon out.HasGPS = true } } } // readIFD reads the directory at the given offset and returns its entries plus // the offset of the next directory, which is unused in practice. func readIFD(block []byte, order binary.ByteOrder, offset uint32) ([]entry, uint32, error) { // Each entry is 12 bytes and a directory of more than a few hundred // entries indicates a malformed or hostile file. const maxEntries = 512 if !inRange(block, offset, 2) { return nil, 0, errors.New("EXIF: directory offset is out of range") } count := order.Uint16(block[offset : offset+2]) if int(count) > maxEntries { return nil, 0, errors.New("EXIF: implausible directory entry count") } entries := make([]entry, 0, count) base := int(offset) + 2 for i := 0; i < int(count); i++ { at := base + i*12 if at < 0 || !inRange(block, uint32(at), 12) { return nil, 0, errors.New("EXIF: directory entry is out of range") } rec := block[at : at+12] e := entry{ tag: tiffTag(order.Uint16(rec[0:2])), typ: order.Uint16(rec[2:4]), count: order.Uint32(rec[4:8]), } copy(e.inline[:], rec[8:12]) entries = append(entries, e) } nextAt := base + int(count)*12 var next uint32 if nextAt >= 0 && inRange(block, uint32(nextAt), 4) { next = order.Uint32(block[nextAt : nextAt+4]) } return entries, next, nil } // exifTimeLayout is the non-conforming timestamp format used by EXIF. const exifTimeLayout = "2006:01:02 15:04:05" // typeSize returns the byte width of one element of a TIFF type. // // RATIONAL and SRATIONAL are two LONGs, so they are eight bytes rather than // four. Getting this wrong makes every subsequent offset in the directory wrong, // which is why it is worth stating explicitly. func typeSize(typ uint16) (int, bool) { switch typ { case tiffByte, tiffASCII, tiffUndefined: return 1, true case tiffShort: return 2, true case tiffLong: return 4, true case tiffRational: return 8, true default: return 0, false } } // payload locates the entry's value, which is either inline in the 4-byte value // field or at a pointer stored in that field. func (e entry) payload(block []byte, order binary.ByteOrder) ([]byte, bool) { size, ok := typeSize(e.typ) if !ok { return nil, false } total := uint64(e.count) * uint64(size) if total == 0 { return nil, false } // More than a few kilobytes of "value" means a malformed file; the fields // this application reads are all short. if total > 4096 { return nil, false } if total <= 4 { return e.inline[:total], true } offset := e.firstValue(order) if !inRange(block, offset, uint32(total)) { return nil, false } return block[int(offset) : int(offset)+int(total)], true } func (e entry) firstValue(order binary.ByteOrder) uint32 { return order.Uint32(e.inline[:4]) } func (e entry) asASCII(block []byte, order binary.ByteOrder) (string, bool) { if e.typ != tiffASCII { return "", false } raw, ok := e.payload(block, order) if !ok { return "", false } s := string(raw) if i := strings.IndexByte(s, 0); i >= 0 { s = s[:i] } return strings.TrimSpace(s), true } // asRationals decodes an array of unsigned rationals. Each is a numerator and // denominator pair, and a zero denominator makes the entry unusable. func (e entry) asRationals(block []byte, order binary.ByteOrder) ([]float64, bool) { if e.typ != tiffRational { return nil, false } raw, ok := e.payload(block, order) if !ok || len(raw)%8 != 0 { return nil, false } n := len(raw) / 8 out := make([]float64, 0, n) for i := 0; i < n; i++ { num := order.Uint32(raw[i*8 : i*8+4]) den := order.Uint32(raw[i*8+4 : i*8+8]) if den == 0 { return nil, false } out = append(out, float64(num)/float64(den)) } return out, true } // dmsToDecimal converts EXIF's degrees/minutes/seconds triple to decimal // degrees. It reports false for out-of-range or non-finite values. func dmsToDecimal(degrees, minutes, seconds float64) (float64, bool) { if math.IsNaN(degrees) || math.IsNaN(minutes) || math.IsNaN(seconds) { return 0, false } if degrees < 0 || degrees > 180 { return 0, false } if minutes < 0 || minutes >= 60 { return 0, false } if seconds < 0 || seconds >= 60 { return 0, false } v := degrees + minutes/60 + seconds/3600 return v, true } func inRange(block []byte, offset, length uint32) bool { end := uint64(offset) + uint64(length) return end <= uint64(len(block)) } func math_isNaN(v float64) bool { return v != v }