diff --git a/exif-read-tool/main.go b/exif-read-tool/main.go index a091a2e..1e63b93 100644 --- a/exif-read-tool/main.go +++ b/exif-read-tool/main.go @@ -117,7 +117,7 @@ func main() { valueString = fmt.Sprintf("%v", value) } } else { - valueString, err = tagType.ResolveAsString(valueContext, true) + valueString, err = valueContext.FormatFirst() log.PanicIf(err) value = valueString diff --git a/exif_test.go b/exif_test.go index 9a02b3d..b473222 100644 --- a/exif_test.go +++ b/exif_test.go @@ -90,7 +90,7 @@ func TestVisit(t *testing.T) { valueString = fmt.Sprintf("%v", value) } } else { - valueString, err = tagType.ResolveAsString(valueContext, true) + valueString, err = valueContext.FormatFirst() log.PanicIf(err) } diff --git a/ifd_builder.go b/ifd_builder.go index 327766f..c289a36 100644 --- a/ifd_builder.go +++ b/ifd_builder.go @@ -129,11 +129,9 @@ func (bt *BuilderTag) String() string { var valueString string if bt.value.IsBytes() == true { - tt := NewTagType(bt.typeId, bt.byteOrder) - var err error - valueString, err = tt.Format(bt.value.Bytes(), false) + valueString, err = Format(bt.value.Bytes(), bt.typeId, false, bt.byteOrder) log.PanicIf(err) } else { valueString = fmt.Sprintf("%v", bt.value) diff --git a/ifd_enumerate.go b/ifd_enumerate.go index 28c0e7b..67bcb5d 100644 --- a/ifd_enumerate.go +++ b/ifd_enumerate.go @@ -294,20 +294,59 @@ func (ie *IfdEnumerate) resolveTagValue(ite *IfdTagEntry) (valueBytes []byte, is return valueBytes, false, nil } +// RawTagVisitorPtr is an optional callback that can get hit for every tag we parse +// through. `addressableData` is the byte array startign after the EXIF header +// (where the offsets of all IFDs and values are calculated from). +// +// This was reimplemented as an interface to allow for simpler change management +// in the future. +type RawTagWalk interface { + Visit(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext *ValueContext) (err error) +} + +type RawTagWalkLegacyWrapper struct { + legacyVisitor RawTagVisitor +} + +func (rtwlw RawTagWalkLegacyWrapper) Visit(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext *ValueContext) (err error) { + return rtwlw.legacyVisitor(fqIfdPath, ifdIndex, tagId, tagType, *valueContext) +} + // RawTagVisitor is an optional callback that can get hit for every tag we parse // through. `addressableData` is the byte array startign after the EXIF header // (where the offsets of all IFDs and values are calculated from). +// +// DEPRECATED(dustin): Use a RawTagWalk instead. type RawTagVisitor func(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext ValueContext) (err error) // ParseIfd decodes the IFD block that we're currently sitting on the first // byte of. -func (ie *IfdEnumerate) ParseIfd(fqIfdPath string, ifdIndex int, ite *IfdTagEnumerator, visitor RawTagVisitor, doDescend bool, resolveValues bool) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) { +func (ie *IfdEnumerate) ParseIfd(fqIfdPath string, ifdIndex int, ite *IfdTagEnumerator, visitor interface{}, doDescend bool, resolveValues bool) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) } }() + var visitorWrapper RawTagWalk + + if visitor != nil { + var ok bool + + visitorWrapper, ok = visitor.(RawTagWalk) + if ok == false { + // Legacy usage. + + // `ok` can be `true` but `legacyVisitor` can still be `nil` (when + // passed as nil). + if legacyVisitor, ok := visitor.(RawTagVisitor); ok == true && legacyVisitor != nil { + visitorWrapper = RawTagWalkLegacyWrapper{ + legacyVisitor: legacyVisitor, + } + } + } + } + tagCount, _, err := ite.getUint16() log.PanicIf(err) @@ -338,7 +377,7 @@ func (ie *IfdEnumerate) ParseIfd(fqIfdPath string, ifdIndex int, ite *IfdTagEnum continue } - if visitor != nil { + if visitorWrapper != nil { tt := NewTagType(tag.TagType, ie.byteOrder) vc := @@ -350,7 +389,7 @@ func (ie *IfdEnumerate) ParseIfd(fqIfdPath string, ifdIndex int, ite *IfdTagEnum tag.TagType, ie.byteOrder) - err := visitor(fqIfdPath, ifdIndex, tag.TagId, tt, vc) + err := visitorWrapper.Visit(fqIfdPath, ifdIndex, tag.TagId, tt, vc) log.PanicIf(err) } @@ -412,7 +451,7 @@ func (ie *IfdEnumerate) parseThumbnail(offsetIte, lengthIte *IfdTagEntry) (thumb } // Scan enumerates the different EXIF's IFD blocks. -func (ie *IfdEnumerate) scan(fqIfdName string, ifdOffset uint32, visitor RawTagVisitor, resolveValues bool) (err error) { +func (ie *IfdEnumerate) scan(fqIfdName string, ifdOffset uint32, visitor interface{}, resolveValues bool) (err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) diff --git a/ifd_tag_entry.go b/ifd_tag_entry.go index bf386f6..7519b4a 100644 --- a/ifd_tag_entry.go +++ b/ifd_tag_entry.go @@ -70,9 +70,7 @@ func (ite IfdTagEntry) ValueString(addressableData []byte, byteOrder binary.Byte value = fmt.Sprintf("%v", valueRaw) } else { - tt := NewTagType(ite.TagType, byteOrder) - - value, err = tt.ResolveAsString(vc, false) + value, err = vc.Format() log.PanicIf(err) } diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..4702db2 --- /dev/null +++ b/parser.go @@ -0,0 +1,190 @@ +package exif + +import ( + "bytes" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +type Parser struct { +} + +func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeByte.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = []uint8(data[:count]) + + return value, nil +} + +// ParseAscii returns a string and auto-strips the trailing NUL character. +func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeAscii.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + if len(data) == 0 || data[count-1] != 0 { + s := string(data[:count]) + typeLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s) + + return s, nil + } else { + // Auto-strip the NUL from the end. It serves no purpose outside of + // encoding semantics. + + return string(data[:count-1]), nil + } +} + +// ParseAsciiNoNul returns a string without any consideration for a trailing NUL +// character. +func (p *Parser) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeAscii.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + return string(data[:count]), nil +} + +func (p *Parser) ParseShorts(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint16, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeShort.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]uint16, count) + for i := 0; i < count; i++ { + value[i] = byteOrder.Uint16(data[i*2:]) + } + + return value, nil +} + +func (p *Parser) ParseLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeLong.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]uint32, count) + for i := 0; i < count; i++ { + value[i] = byteOrder.Uint32(data[i*4:]) + } + + return value, nil +} + +func (p *Parser) ParseRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []Rational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeRational.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]Rational, count) + for i := 0; i < count; i++ { + value[i].Numerator = byteOrder.Uint32(data[i*8:]) + value[i].Denominator = byteOrder.Uint32(data[i*8+4:]) + } + + return value, nil +} + +func (p *Parser) ParseSignedLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []int32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeSignedLong.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + b := bytes.NewBuffer(data) + + value = make([]int32, count) + for i := 0; i < count; i++ { + err := binary.Read(b, byteOrder, &value[i]) + log.PanicIf(err) + } + + return value, nil +} + +func (p *Parser) ParseSignedRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []SignedRational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) < (TypeSignedRational.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + b := bytes.NewBuffer(data) + + value = make([]SignedRational, count) + for i := 0; i < count; i++ { + err = binary.Read(b, byteOrder, &value[i].Numerator) + log.PanicIf(err) + + err = binary.Read(b, byteOrder, &value[i].Denominator) + log.PanicIf(err) + } + + return value, nil +} diff --git a/tag_type.go b/tag_type.go new file mode 100644 index 0000000..3661778 --- /dev/null +++ b/tag_type.go @@ -0,0 +1,397 @@ +package exif + +// NOTE(dustin): Most of this file encapsulates deprecated functionality and awaits being dumped in a future release. + +import ( + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +type TagType struct { + tagType TagTypePrimitive + name string + byteOrder binary.ByteOrder +} + +func NewTagType(tagType TagTypePrimitive, byteOrder binary.ByteOrder) TagType { + name, found := TypeNames[tagType] + if found == false { + log.Panicf("tag-type not valid: 0x%04x", tagType) + } + + return TagType{ + tagType: tagType, + name: name, + byteOrder: byteOrder, + } +} + +func (tt TagType) String() string { + return fmt.Sprintf("TagType", tt.name) +} + +func (tt TagType) Name() string { + return tt.name +} + +func (tt TagType) Type() TagTypePrimitive { + return tt.tagType +} + +func (tt TagType) ByteOrder() binary.ByteOrder { + return tt.byteOrder +} + +func (tt TagType) Size() int { + + // DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly. + + return tt.Type().Size() +} + +// valueIsEmbedded will return a boolean indicating whether the value should be +// found directly within the IFD entry or an offset to somewhere else. +func (tt TagType) valueIsEmbedded(unitCount uint32) bool { + return (tt.tagType.Size() * int(unitCount)) <= 4 +} + +func (tt TagType) readRawEncoded(valueContext ValueContext) (rawBytes []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + unitSizeRaw := uint32(tt.tagType.Size()) + + if tt.valueIsEmbedded(valueContext.UnitCount()) == true { + byteLength := unitSizeRaw * valueContext.UnitCount() + return valueContext.RawValueOffset()[:byteLength], nil + } else { + return valueContext.AddressableData()[valueContext.ValueOffset() : valueContext.ValueOffset()+valueContext.UnitCount()*unitSizeRaw], nil + } +} + +func (tt TagType) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseBytes()` should be used. + + value, err = parser.ParseBytes(data, unitCount) + log.PanicIf(err) + + return value, nil +} + +// ParseAscii returns a string and auto-strips the trailing NUL character. +func (tt TagType) ParseAscii(data []byte, unitCount uint32) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseAscii()` should be used. + + value, err = parser.ParseAscii(data, unitCount) + log.PanicIf(err) + + return value, nil +} + +// ParseAsciiNoNul returns a string without any consideration for a trailing NUL +// character. +func (tt TagType) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseAsciiNoNul()` should be used. + + value, err = parser.ParseAsciiNoNul(data, unitCount) + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ParseShorts(data []byte, unitCount uint32) (value []uint16, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseShorts()` should be used. + + value, err = parser.ParseShorts(data, unitCount, tt.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ParseLongs(data []byte, unitCount uint32) (value []uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseLongs()` should be used. + + value, err = parser.ParseLongs(data, unitCount, tt.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ParseRationals(data []byte, unitCount uint32) (value []Rational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseRationals()` should be used. + + value, err = parser.ParseRationals(data, unitCount, tt.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ParseSignedLongs(data []byte, unitCount uint32) (value []int32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseSignedLongs()` should be used. + + value, err = parser.ParseSignedLongs(data, unitCount, tt.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ParseSignedRationals(data []byte, unitCount uint32) (value []SignedRational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(*Parser).ParseSignedRationals()` should be used. + + value, err = parser.ParseSignedRationals(data, unitCount, tt.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadByteValues(valueContext ValueContext) (value []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadBytes()` should be used. + + value, err = valueContext.ReadBytes() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadAsciiValue(valueContext ValueContext) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadAscii()` should be used. + + value, err = valueContext.ReadAscii() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadAsciiNoNulValue(valueContext ValueContext) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadAsciiNoNul()` should be used. + + value, err = valueContext.ReadAsciiNoNul() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadShortValues(valueContext ValueContext) (value []uint16, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadShorts()` should be used. + + value, err = valueContext.ReadShorts() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadLongValues(valueContext ValueContext) (value []uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadLongs()` should be used. + + value, err = valueContext.ReadLongs() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadRationalValues(valueContext ValueContext) (value []Rational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadRationals()` should be used. + + value, err = valueContext.ReadRationals() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadSignedLongValues(valueContext ValueContext) (value []int32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadSignedLongs()` should be used. + + value, err = valueContext.ReadSignedLongs() + log.PanicIf(err) + + return value, nil +} + +func (tt TagType) ReadSignedRationalValues(valueContext ValueContext) (value []SignedRational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).ReadSignedRationals()` should be used. + + value, err = valueContext.ReadSignedRationals() + log.PanicIf(err) + + return value, nil +} + +// ResolveAsString resolves the given value and returns a flat string. +// +// Where the type is not ASCII, `justFirst` indicates whether to just stringify +// the first item in the slice (or return an empty string if the slice is +// empty). +// +// Since this method lacks the information to process unknown-type tags (e.g. +// byte-order, tag-ID, IFD type), it will return an error if attempted. See +// `UndefinedValue()`. +func (tt TagType) ResolveAsString(valueContext ValueContext, justFirst bool) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if justFirst == true { + value, err = valueContext.FormatFirst() + log.PanicIf(err) + } else { + value, err = valueContext.Format() + log.PanicIf(err) + } + + return value, nil +} + +// Resolve knows how to resolve the given value. +// +// Since this method lacks the information to process unknown-type tags (e.g. +// byte-order, tag-ID, IFD type), it will return an error if attempted. See +// `UndefinedValue()`. +func (tt TagType) Resolve(valueContext *ValueContext) (values interface{}, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `(ValueContext).Values()` should be used. + + values, err = valueContext.Values() + log.PanicIf(err) + + return values, nil +} + +// Encode knows how to encode the given value to a byte slice. +func (tt TagType) Encode(value interface{}) (encoded []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + ve := NewValueEncoder(tt.byteOrder) + + ed, err := ve.EncodeWithType(tt, value) + log.PanicIf(err) + + return ed.Encoded, err +} + +func (tt TagType) FromString(valueString string) (value interface{}, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // DEPRECATED(dustin): `EncodeStringToBytes()` should be used. + + value, err = EncodeStringToBytes(tt.tagType, valueString) + log.PanicIf(err) + + return value, nil +} diff --git a/tags_unknown.go b/tags_unknown.go index 4a4bcc0..6f62ffa 100644 --- a/tags_unknown.go +++ b/tags_unknown.go @@ -226,40 +226,53 @@ func (tutuv TagUnknownType_UnknownValue) String() string { } // UndefinedValue knows how to resolve the value for most unknown-type tags. -func UndefinedValue(ifdPath string, tagId uint16, valueContext ValueContext, byteOrder binary.ByteOrder) (value interface{}, err error) { +func UndefinedValue(ifdPath string, tagId uint16, valueContext interface{}, byteOrder binary.ByteOrder) (value interface{}, err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) } }() + var valueContextPtr *ValueContext + + if vc, ok := valueContext.(*ValueContext); ok == true { + // Legacy usage. + + valueContextPtr = vc + } else { + // Standard usage. + + valueContextValue := valueContext.(ValueContext) + valueContextPtr = &valueContextValue + } + typeLogger.Debugf(nil, "UndefinedValue: IFD-PATH=[%s] TAG-ID=(0x%02x)", ifdPath, tagId) if ifdPath == IfdPathStandardExif { if tagId == 0x9000 { // ExifVersion - tt := NewTagType(TypeAsciiNoNul, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeAsciiNoNul, valueContextPtr.UnitCount()) - valueString, err := tt.ReadAsciiNoNulValue(valueContext) + valueString, err := valueContextPtr.ReadAsciiNoNul() log.PanicIf(err) return TagUnknownType_GeneralString(valueString), nil } else if tagId == 0xa000 { // FlashpixVersion - tt := NewTagType(TypeAsciiNoNul, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeAsciiNoNul, valueContextPtr.UnitCount()) - valueString, err := tt.ReadAsciiNoNulValue(valueContext) + valueString, err := valueContextPtr.ReadAsciiNoNul() log.PanicIf(err) return TagUnknownType_GeneralString(valueString), nil } else if tagId == 0x9286 { // UserComment - tt := NewTagType(TypeByte, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeByte, valueContextPtr.UnitCount()) - valueBytes, err := tt.ReadByteValues(valueContext) + valueBytes, err := valueContextPtr.ReadBytes() log.PanicIf(err) unknownUc := TagUnknownType_9298_UserComment{ @@ -286,9 +299,9 @@ func UndefinedValue(ifdPath string, tagId uint16, valueContext ValueContext, byt // TODO(dustin): !! This is the Wild Wild West. This very well might be a child IFD, but any and all OEM's define their own formats. If we're going to be writing changes and this is complete EXIF (which may not have the first eight bytes), it might be fine. However, if these are just IFDs they'll be relative to the main EXIF, this will invalidate the MakerNote data for IFDs and any other implementations that use offsets unless we can interpret them all. It be best to return to this later and just exclude this from being written for now, though means a loss of a wealth of image metadata. // -> We can also just blindly try to interpret as an IFD and just validate that it's looks good (maybe it will even have a 'next ifd' pointer that we can validate is 0x0). - tt := NewTagType(TypeByte, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeByte, valueContextPtr.UnitCount()) - valueBytes, err := tt.ReadByteValues(valueContext) + valueBytes, err := valueContextPtr.ReadBytes() log.PanicIf(err) // TODO(dustin): Doesn't work, but here as an example. @@ -313,9 +326,9 @@ func UndefinedValue(ifdPath string, tagId uint16, valueContext ValueContext, byt } else if tagId == 0x9101 { // ComponentsConfiguration - tt := NewTagType(TypeByte, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeByte, valueContextPtr.UnitCount()) - valueBytes, err := tt.ReadByteValues(valueContext) + valueBytes, err := valueContextPtr.ReadBytes() log.PanicIf(err) for configurationId, configurationBytes := range TagUnknownType_9101_ComponentsConfiguration_Configurations { @@ -340,18 +353,18 @@ func UndefinedValue(ifdPath string, tagId uint16, valueContext ValueContext, byt if tagId == 0x001c { // GPSAreaInformation - tt := NewTagType(TypeAsciiNoNul, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeAsciiNoNul, valueContextPtr.UnitCount()) - valueString, err := tt.ReadAsciiNoNulValue(valueContext) + valueString, err := valueContextPtr.ReadAsciiNoNul() log.PanicIf(err) return TagUnknownType_GeneralString(valueString), nil } else if tagId == 0x001b { // GPSProcessingMethod - tt := NewTagType(TypeAsciiNoNul, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeAsciiNoNul, valueContextPtr.UnitCount()) - valueString, err := tt.ReadAsciiNoNulValue(valueContext) + valueString, err := valueContextPtr.ReadAsciiNoNul() log.PanicIf(err) return TagUnknownType_GeneralString(valueString), nil @@ -360,9 +373,9 @@ func UndefinedValue(ifdPath string, tagId uint16, valueContext ValueContext, byt if tagId == 0x0002 { // InteropVersion - tt := NewTagType(TypeAsciiNoNul, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeAsciiNoNul, valueContextPtr.UnitCount()) - valueString, err := tt.ReadAsciiNoNulValue(valueContext) + valueString, err := valueContextPtr.ReadAsciiNoNul() log.PanicIf(err) return TagUnknownType_GeneralString(valueString), nil @@ -379,9 +392,9 @@ func UndefinedValue(ifdPath string, tagId uint16, valueContext ValueContext, byt // Return encapsulated data rather than an error so that we can at least // print/profile the opaque data. - tt := NewTagType(TypeByte, byteOrder) + valueContextPtr.SetUnknownValueParameters(TypeByte, valueContextPtr.UnitCount()) - valueBytes, err := tt.ReadByteValues(valueContext) + valueBytes, err := valueContextPtr.ReadBytes() log.PanicIf(err) tutuv := TagUnknownType_UnknownValue(valueBytes) diff --git a/type.go b/type.go index a25cbf1..0d635b7 100644 --- a/type.go +++ b/type.go @@ -1,7 +1,6 @@ package exif import ( - "bytes" "errors" "fmt" "strconv" @@ -14,6 +13,10 @@ import ( type TagTypePrimitive uint16 +func (typeType TagTypePrimitive) String() string { + return TypeNames[typeType] +} + func (tagType TagTypePrimitive) Size() int { if tagType == TypeByte { return 1 @@ -39,16 +42,16 @@ func (tagType TagTypePrimitive) Size() int { const ( TypeByte TagTypePrimitive = 1 - TypeAscii = 2 - TypeShort = 3 - TypeLong = 4 - TypeRational = 5 - TypeUndefined = 7 - TypeSignedLong = 9 - TypeSignedRational = 10 + TypeAscii TagTypePrimitive = 2 + TypeShort TagTypePrimitive = 3 + TypeLong TagTypePrimitive = 4 + TypeRational TagTypePrimitive = 5 + TypeUndefined TagTypePrimitive = 7 + TypeSignedLong TagTypePrimitive = 9 + TypeSignedRational TagTypePrimitive = 10 // TypeAsciiNoNul is just a pseudo-type, for our own purposes. - TypeAsciiNoNul = 0xf0 + TypeAsciiNoNul TagTypePrimitive = 0xf0 ) var ( @@ -98,541 +101,16 @@ type SignedRational struct { Denominator int32 } -type TagType struct { - tagType TagTypePrimitive - name string - byteOrder binary.ByteOrder -} - -func NewTagType(tagType TagTypePrimitive, byteOrder binary.ByteOrder) TagType { - name, found := TypeNames[tagType] - if found == false { - log.Panicf("tag-type not valid: 0x%04x", tagType) - } - - return TagType{ - tagType: tagType, - name: name, - byteOrder: byteOrder, - } -} - -func (tt TagType) String() string { - return fmt.Sprintf("TagType", tt.name) -} - -func (tt TagType) Name() string { - return tt.name -} - -func (tt TagType) Type() TagTypePrimitive { - return tt.tagType -} - -func (tt TagType) ByteOrder() binary.ByteOrder { - return tt.byteOrder -} - -// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly. -func (tt TagType) Size() int { - return tt.Type().Size() -} - -// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly. func TagTypeSize(tagType TagTypePrimitive) int { + + // DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly. + return tagType.Size() } -// valueIsEmbedded will return a boolean indicating whether the value should be -// found directly within the IFD entry or an offset to somewhere else. -func (tt TagType) valueIsEmbedded(unitCount uint32) bool { - return (tt.tagType.Size() * int(unitCount)) <= 4 -} - -func (tt TagType) readRawEncoded(valueContext ValueContext) (rawBytes []byte, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - unitSizeRaw := uint32(tt.tagType.Size()) - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - byteLength := unitSizeRaw * valueContext.UnitCount() - return valueContext.RawValueOffset()[:byteLength], nil - } else { - return valueContext.AddressableData()[valueContext.ValueOffset() : valueContext.ValueOffset()+valueContext.UnitCount()*unitSizeRaw], nil - } -} - -func (tt TagType) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeByte { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - value = []uint8(data[:count]) - - return value, nil -} - -// ParseAscii returns a string and auto-strips the trailing NUL character. -func (tt TagType) ParseAscii(data []byte, unitCount uint32) (value string, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeAscii && tt.tagType != TypeAsciiNoNul { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - if len(data) == 0 || data[count-1] != 0 { - s := string(data[:count]) - typeLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s) - - return s, nil - } else { - // Auto-strip the NUL from the end. It serves no purpose outside of - // encoding semantics. - - return string(data[:count-1]), nil - } -} - -// ParseAsciiNoNul returns a string without any consideration for a trailing NUL -// character. -func (tt TagType) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeAscii && tt.tagType != TypeAsciiNoNul { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - return string(data[:count]), nil -} - -func (tt TagType) ParseShorts(data []byte, unitCount uint32) (value []uint16, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeShort { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - value = make([]uint16, count) - for i := 0; i < count; i++ { - if tt.byteOrder == binary.BigEndian { - value[i] = binary.BigEndian.Uint16(data[i*2:]) - } else { - value[i] = binary.LittleEndian.Uint16(data[i*2:]) - } - } - - return value, nil -} - -func (tt TagType) ParseLongs(data []byte, unitCount uint32) (value []uint32, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeLong { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - value = make([]uint32, count) - for i := 0; i < count; i++ { - if tt.byteOrder == binary.BigEndian { - value[i] = binary.BigEndian.Uint32(data[i*4:]) - } else { - value[i] = binary.LittleEndian.Uint32(data[i*4:]) - } - } - - return value, nil -} - -func (tt TagType) ParseRationals(data []byte, unitCount uint32) (value []Rational, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeRational { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - value = make([]Rational, count) - for i := 0; i < count; i++ { - if tt.byteOrder == binary.BigEndian { - value[i].Numerator = binary.BigEndian.Uint32(data[i*8:]) - value[i].Denominator = binary.BigEndian.Uint32(data[i*8+4:]) - } else { - value[i].Numerator = binary.LittleEndian.Uint32(data[i*8:]) - value[i].Denominator = binary.LittleEndian.Uint32(data[i*8+4:]) - } - } - - return value, nil -} - -func (tt TagType) ParseSignedLongs(data []byte, unitCount uint32) (value []int32, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeSignedLong { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - b := bytes.NewBuffer(data) - - value = make([]int32, count) - for i := 0; i < count; i++ { - if tt.byteOrder == binary.BigEndian { - err := binary.Read(b, binary.BigEndian, &value[i]) - log.PanicIf(err) - } else { - err := binary.Read(b, binary.LittleEndian, &value[i]) - log.PanicIf(err) - } - } - - return value, nil -} - -func (tt TagType) ParseSignedRationals(data []byte, unitCount uint32) (value []SignedRational, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType != TypeSignedRational { - log.Panic(ErrWrongType) - } - - count := int(unitCount) - - if len(data) < (tt.tagType.Size() * count) { - log.Panic(ErrNotEnoughData) - } - - b := bytes.NewBuffer(data) - - value = make([]SignedRational, count) - for i := 0; i < count; i++ { - if tt.byteOrder == binary.BigEndian { - err = binary.Read(b, binary.BigEndian, &value[i].Numerator) - log.PanicIf(err) - - err = binary.Read(b, binary.BigEndian, &value[i].Denominator) - log.PanicIf(err) - } else { - err = binary.Read(b, binary.LittleEndian, &value[i].Numerator) - log.PanicIf(err) - - err = binary.Read(b, binary.LittleEndian, &value[i].Denominator) - log.PanicIf(err) - } - } - - return value, nil -} - -func (tt TagType) ReadByteValues(valueContext ValueContext) (value []byte, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading BYTE value (embedded).") - - // In this case, the bytes normally used for the offset are actually - // data. - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseBytes(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading BYTE value (at offset).") - - value, err = tt.ParseBytes(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadAsciiValue(valueContext ValueContext) (value string, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading ASCII value (embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseAscii(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading ASCII value (at offset).") - - value, err = tt.ParseAscii(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadAsciiNoNulValue(valueContext ValueContext) (value string, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading ASCII value (no-nul; embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseAsciiNoNul(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading ASCII value (no-nul; at offset).") - - value, err = tt.ParseAsciiNoNul(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadShortValues(valueContext ValueContext) (value []uint16, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading SHORT value (embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseShorts(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading SHORT value (at offset).") - - value, err = tt.ParseShorts(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadLongValues(valueContext ValueContext) (value []uint32, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading LONG value (embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseLongs(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading LONG value (at offset).") - - value, err = tt.ParseLongs(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadRationalValues(valueContext ValueContext) (value []Rational, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading RATIONAL value (embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseRationals(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading RATIONAL value (at offset).") - - value, err = tt.ParseRationals(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadSignedLongValues(valueContext ValueContext) (value []int32, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading SLONG value (embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseSignedLongs(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading SLONG value (at offset).") - - value, err = tt.ParseSignedLongs(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -func (tt TagType) ReadSignedRationalValues(valueContext ValueContext) (value []SignedRational, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.valueIsEmbedded(valueContext.UnitCount()) == true { - typeLogger.Debugf(nil, "Reading SRATIONAL value (embedded).") - - byteLength := uint32(tt.tagType.Size()) * valueContext.UnitCount() - rawValue := valueContext.RawValueOffset()[:byteLength] - - value, err = tt.ParseSignedRationals(rawValue, valueContext.UnitCount()) - log.PanicIf(err) - } else { - typeLogger.Debugf(nil, "Reading SRATIONAL value (at offset).") - - value, err = tt.ParseSignedRationals(valueContext.AddressableData()[valueContext.ValueOffset():], valueContext.UnitCount()) - log.PanicIf(err) - } - - return value, nil -} - -// ResolveAsString resolves the given value and returns a flat string. -// -// Where the type is not ASCII, `justFirst` indicates whether to just stringify -// the first item in the slice (or return an empty string if the slice is -// empty). -// -// Since this method lacks the information to process unknown-type tags (e.g. -// byte-order, tag-ID, IFD type), it will return an error if attempted. See -// `UndefinedValue()`. -func (tt TagType) ResolveAsString(valueContext ValueContext, justFirst bool) (value string, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - rawBytes, err := tt.readRawEncoded(valueContext) - log.PanicIf(err) - - valueString, err := tt.Format(rawBytes, justFirst) - log.PanicIf(err) - - return valueString, nil -} - // Format returns a stringified value for the given bytes. Automatically // calculates count based on type size. -func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err error) { +func Format(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (value string, err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) @@ -641,11 +119,10 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err // TODO(dustin): !! Add tests - typeId := tt.Type() - typeSize := typeId.Size() + typeSize := tagType.Size() if len(rawBytes)%typeSize != 0 { - log.Panicf("byte-count (%d) does not align for [%s] type with a size of (%d) bytes", len(rawBytes), TypeNames[typeId], typeSize) + log.Panicf("byte-count (%d) does not align for [%s] type with a size of (%d) bytes", len(rawBytes), TypeNames[tagType], typeSize) } // unitCount is the calculated unit-count. This should equal the original @@ -655,28 +132,28 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err // Truncate the items if it's not bytes or a string and we just want the first. valueSuffix := "" - if justFirst == true && unitCount > 1 && typeId != TypeByte && typeId != TypeAscii && typeId != TypeAsciiNoNul { + if justFirst == true && unitCount > 1 && tagType != TypeByte && tagType != TypeAscii && tagType != TypeAsciiNoNul { unitCount = 1 valueSuffix = "..." } - if typeId == TypeByte { - items, err := tt.ParseBytes(rawBytes, unitCount) + if tagType == TypeByte { + items, err := parser.ParseBytes(rawBytes, unitCount) log.PanicIf(err) return DumpBytesToString(items), nil - } else if typeId == TypeAscii { - phrase, err := tt.ParseAscii(rawBytes, unitCount) + } else if tagType == TypeAscii { + phrase, err := parser.ParseAscii(rawBytes, unitCount) log.PanicIf(err) return phrase, nil - } else if typeId == TypeAsciiNoNul { - phrase, err := tt.ParseAsciiNoNul(rawBytes, unitCount) + } else if tagType == TypeAsciiNoNul { + phrase, err := parser.ParseAsciiNoNul(rawBytes, unitCount) log.PanicIf(err) return phrase, nil - } else if typeId == TypeShort { - items, err := tt.ParseShorts(rawBytes, unitCount) + } else if tagType == TypeShort { + items, err := parser.ParseShorts(rawBytes, unitCount, byteOrder) log.PanicIf(err) if len(items) > 0 { @@ -688,8 +165,8 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err } else { return "", nil } - } else if typeId == TypeLong { - items, err := tt.ParseLongs(rawBytes, unitCount) + } else if tagType == TypeLong { + items, err := parser.ParseLongs(rawBytes, unitCount, byteOrder) log.PanicIf(err) if len(items) > 0 { @@ -701,8 +178,8 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err } else { return "", nil } - } else if typeId == TypeRational { - items, err := tt.ParseRationals(rawBytes, unitCount) + } else if tagType == TypeRational { + items, err := parser.ParseRationals(rawBytes, unitCount, byteOrder) log.PanicIf(err) if len(items) > 0 { @@ -719,8 +196,8 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err } else { return "", nil } - } else if typeId == TypeSignedLong { - items, err := tt.ParseSignedLongs(rawBytes, unitCount) + } else if tagType == TypeSignedLong { + items, err := parser.ParseSignedLongs(rawBytes, unitCount, byteOrder) log.PanicIf(err) if len(items) > 0 { @@ -732,8 +209,8 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err } else { return "", nil } - } else if typeId == TypeSignedRational { - items, err := tt.ParseSignedRationals(rawBytes, unitCount) + } else if tagType == TypeSignedRational { + items, err := parser.ParseSignedRationals(rawBytes, unitCount, byteOrder) log.PanicIf(err) parts := make([]string, len(items)) @@ -752,113 +229,44 @@ func (tt TagType) Format(rawBytes []byte, justFirst bool) (value string, err err } } else { // Affects only "unknown" values, in general. - log.Panicf("value of type (%d) [%s] can not be formatted into string", typeId, tt) + log.Panicf("value of type [%s] can not be formatted into string", tagType.String()) // Never called. return "", nil } } -// Value knows how to resolve the given value. -// -// Since this method lacks the information to process unknown-type tags (e.g. -// byte-order, tag-ID, IFD type), it will return an error if attempted. See -// `UndefinedValue()`. -func (tt TagType) Resolve(valueContext ValueContext) (value interface{}, err error) { +func EncodeStringToBytes(tagType TagTypePrimitive, valueString string) (value interface{}, err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) } }() - typeId := tt.Type() - - if typeId == TypeByte { - value, err = tt.ReadByteValues(valueContext) - log.PanicIf(err) - } else if typeId == TypeAscii { - value, err = tt.ReadAsciiValue(valueContext) - log.PanicIf(err) - } else if typeId == TypeAsciiNoNul { - value, err = tt.ReadAsciiNoNulValue(valueContext) - log.PanicIf(err) - } else if typeId == TypeShort { - value, err = tt.ReadShortValues(valueContext) - log.PanicIf(err) - } else if typeId == TypeLong { - value, err = tt.ReadLongValues(valueContext) - log.PanicIf(err) - } else if typeId == TypeRational { - value, err = tt.ReadRationalValues(valueContext) - log.PanicIf(err) - } else if typeId == TypeSignedLong { - value, err = tt.ReadSignedLongValues(valueContext) - log.PanicIf(err) - } else if typeId == TypeSignedRational { - value, err = tt.ReadSignedRationalValues(valueContext) - log.PanicIf(err) - } else if typeId == TypeUndefined { - log.Panicf("will not parse unknown-type value: %v", tt) - - // Never called. - return nil, nil - } else { - log.Panicf("value of type (%d) [%s] is unparseable", typeId, tt) - - // Never called. - return nil, nil - } - - return value, nil -} - -// Encode knows how to encode the given value to a byte slice. -func (tt TagType) Encode(value interface{}) (encoded []byte, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - ve := NewValueEncoder(tt.byteOrder) - - ed, err := ve.EncodeWithType(tt, value) - log.PanicIf(err) - - return ed.Encoded, err -} - -func (tt TagType) FromString(valueString string) (value interface{}, err error) { - defer func() { - if state := recover(); state != nil { - err = log.Wrap(state.(error)) - } - }() - - if tt.tagType == TypeUndefined { + if tagType == TypeUndefined { // TODO(dustin): Circle back to this. log.Panicf("undefined-type values are not supported") } - if tt.tagType == TypeByte { + if tagType == TypeByte { return []byte(valueString), nil - } else if tt.tagType == TypeAscii || tt.tagType == TypeAsciiNoNul { + } else if tagType == TypeAscii || tagType == TypeAsciiNoNul { // Whether or not we're putting an NUL on the end is only relevant for // byte-level encoding. This function really just supports a user // interface. return valueString, nil - } else if tt.tagType == TypeShort { + } else if tagType == TypeShort { n, err := strconv.ParseUint(valueString, 10, 16) log.PanicIf(err) return uint16(n), nil - } else if tt.tagType == TypeLong { + } else if tagType == TypeLong { n, err := strconv.ParseUint(valueString, 10, 32) log.PanicIf(err) return uint32(n), nil - } else if tt.tagType == TypeRational { + } else if tagType == TypeRational { parts := strings.SplitN(valueString, "/", 2) numerator, err := strconv.ParseUint(parts[0], 10, 32) @@ -871,12 +279,12 @@ func (tt TagType) FromString(valueString string) (value interface{}, err error) Numerator: uint32(numerator), Denominator: uint32(denominator), }, nil - } else if tt.tagType == TypeSignedLong { + } else if tagType == TypeSignedLong { n, err := strconv.ParseInt(valueString, 10, 32) log.PanicIf(err) return int32(n), nil - } else if tt.tagType == TypeSignedRational { + } else if tagType == TypeSignedRational { parts := strings.SplitN(valueString, "/", 2) numerator, err := strconv.ParseInt(parts[0], 10, 32) @@ -891,7 +299,7 @@ func (tt TagType) FromString(valueString string) (value interface{}, err error) }, nil } - log.Panicf("from-string encoding for type not supported; this shouldn't happen: (%d)", tt.Type()) + log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String()) return nil, nil } diff --git a/value_context.go b/value_context.go index 4d60c04..4c187dc 100644 --- a/value_context.go +++ b/value_context.go @@ -2,6 +2,12 @@ package exif import ( "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +var ( + parser *Parser ) // ValueContext describes all of the parameters required to find and extract @@ -14,26 +20,18 @@ type ValueContext struct { tagType TagTypePrimitive byteOrder binary.ByteOrder + + // undefinedValueTagType is the effective type to use if this is an "undefined" + // value. + undefinedValueTagType TagTypePrimitive + + // undefinedValueUnitCount is the effective unit-count to use if this is an + // "undefined" value. + undefinedValueUnitCount uint32 } -func (vc ValueContext) UnitCount() uint32 { - return vc.unitCount -} - -func (vc ValueContext) ValueOffset() uint32 { - return vc.valueOffset -} - -func (vc ValueContext) RawValueOffset() []byte { - return vc.rawValueOffset -} - -func (vc ValueContext) AddressableData() []byte { - return vc.addressableData -} - -func newValueContext(unitCount, valueOffset uint32, rawValueOffset, addressableData []byte, tagType TagTypePrimitive, byteOrder binary.ByteOrder) ValueContext { - return ValueContext{ +func newValueContext(unitCount, valueOffset uint32, rawValueOffset, addressableData []byte, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext { + return &ValueContext{ unitCount: unitCount, valueOffset: valueOffset, rawValueOffset: rawValueOffset, @@ -43,3 +41,295 @@ func newValueContext(unitCount, valueOffset uint32, rawValueOffset, addressableD byteOrder: byteOrder, } } + +func (vc *ValueContext) SetUnknownValueParameters(tagType TagTypePrimitive, unitCount uint32) { + vc.undefinedValueTagType = tagType + vc.undefinedValueUnitCount = unitCount +} + +func (vc *ValueContext) UnitCount() uint32 { + return vc.unitCount +} + +func (vc *ValueContext) ValueOffset() uint32 { + return vc.valueOffset +} + +func (vc *ValueContext) RawValueOffset() []byte { + return vc.rawValueOffset +} + +func (vc *ValueContext) AddressableData() []byte { + return vc.addressableData +} + +// isEmbedded returns whether the value is embedded or a reference. This can't +// be precalculated since the size is not defined for all types (namely the +// "undefined" types). +func (vc *ValueContext) isEmbedded() bool { + tagType, unitCount := vc.effectiveValueParameters() + + return (tagType.Size() * int(unitCount)) <= 4 +} + +func (vc *ValueContext) effectiveValueParameters() (tagType TagTypePrimitive, unitCount uint32) { + if vc.tagType == TypeUndefined { + tagType = vc.undefinedValueTagType + unitCount = vc.undefinedValueUnitCount + + if tagType == 0 { + log.Panicf("undefined-value type not set") + } + } else { + tagType = vc.tagType + unitCount = vc.unitCount + } + + return tagType, unitCount +} + +func (vc *ValueContext) readRawEncoded() (rawBytes []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + tagType, unitCount := vc.effectiveValueParameters() + + unitSizeRaw := uint32(tagType.Size()) + + if vc.isEmbedded() == true { + byteLength := unitSizeRaw * unitCount + return vc.rawValueOffset[:byteLength], nil + } else { + return vc.addressableData[vc.valueOffset : vc.valueOffset+unitCount*unitSizeRaw], nil + } +} + +// Format returns a string representation for the value. +// +// Where the type is not ASCII, `justFirst` indicates whether to just stringify +// the first item in the slice (or return an empty string if the slice is +// empty). +// +// Since this method lacks the information to process undefined-type tags (e.g. +// byte-order, tag-ID, IFD type), it will return an error if attempted. See +// `UndefinedValue()`. +func (vc *ValueContext) Format() (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawBytes, err := vc.readRawEncoded() + log.PanicIf(err) + + phrase, err := Format(rawBytes, vc.tagType, false, vc.byteOrder) + log.PanicIf(err) + + return phrase, nil +} + +// FormatOne is similar to `Format` but only gets and stringifies the first +// item. +func (vc *ValueContext) FormatFirst() (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawBytes, err := vc.readRawEncoded() + log.PanicIf(err) + + phrase, err := Format(rawBytes, vc.tagType, true, vc.byteOrder) + log.PanicIf(err) + + return phrase, nil +} + +func (vc *ValueContext) ReadBytes() (value []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseBytes(rawValue, vc.unitCount) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadAscii() (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseAscii(rawValue, vc.unitCount) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadAsciiNoNul() (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseAsciiNoNul(rawValue, vc.unitCount) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadShorts() (value []uint16, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseShorts(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadLongs() (value []uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseLongs(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadRationals() (value []Rational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseRationals(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadSignedLongs() (value []int32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseSignedLongs(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +func (vc *ValueContext) ReadSignedRationals() (value []SignedRational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseSignedRationals(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +// Values knows how to resolve the given value. This value is always a list +// (undefined-values aside), so we're named accordingly. +// +// Since this method lacks the information to process unknown-type tags (e.g. +// byte-order, tag-ID, IFD type), it will return an error if attempted. See +// `UndefinedValue()`. +func (vc *ValueContext) Values() (value interface{}, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if vc.tagType == TypeByte { + value, err = vc.ReadBytes() + log.PanicIf(err) + } else if vc.tagType == TypeAscii { + value, err = vc.ReadAscii() + log.PanicIf(err) + } else if vc.tagType == TypeAsciiNoNul { + value, err = vc.ReadAsciiNoNul() + log.PanicIf(err) + } else if vc.tagType == TypeShort { + value, err = vc.ReadShorts() + log.PanicIf(err) + } else if vc.tagType == TypeLong { + value, err = vc.ReadLongs() + log.PanicIf(err) + } else if vc.tagType == TypeRational { + value, err = vc.ReadRationals() + log.PanicIf(err) + } else if vc.tagType == TypeSignedLong { + value, err = vc.ReadSignedLongs() + log.PanicIf(err) + } else if vc.tagType == TypeSignedRational { + value, err = vc.ReadSignedRationals() + log.PanicIf(err) + } else if vc.tagType == TypeUndefined { + log.Panicf("will not parse undefined-type value") + + // Never called. + return nil, nil + } else { + log.Panicf("value of type [%s] is unparseable", vc.tagType) + + // Never called. + return nil, nil + } + + return value, nil +} + +func init() { + parser = &Parser{} +}