ifd_builder_encode: Added tests for child-IFD encoding and allocation.

- ifd_enumerate: Added utility functions to parse individual, encoded
  IFDs and tags.
- ifd_builder: Corrected `NewBuilderTagFromConfig()` to automatically
  encode whichever type of value-argument it gets.
This commit is contained in:
Dustin Oprea 2018-04-28 19:22:13 -04:00
parent 9db558a82d
commit 18d527eb72
4 changed files with 240 additions and 30 deletions

View File

@ -92,24 +92,33 @@ func (bt builderTag) String() string {
}
// NewBuilderTagFromConfig allows us to easily generate solid, consistent tags
// for testing with.
// for testing with. `ii` is the tpye of IFD that owns this tag.
func NewBuilderTagFromConfig(ii IfdIdentity, tagId uint16, byteOrder binary.ByteOrder, value interface{}) builderTag {
ti := NewTagIndex()
var tagValue *IfdBuilderTagValue
it, err := ti.Get(ii, tagId)
log.PanicIf(err)
switch value.(type) {
case *IfdBuilder:
tagValue = NewIfdBuilderTagValueFromIfdBuilder(value.(*IfdBuilder))
default:
ti := NewTagIndex()
tt := NewTagType(it.Type, byteOrder)
it, err := ti.Get(ii, tagId)
log.PanicIf(err)
ve := NewValueEncoder(byteOrder)
tt := NewTagType(it.Type, byteOrder)
ed, err := ve.EncodeWithType(tt, value)
log.PanicIf(err)
ve := NewValueEncoder(byteOrder)
ed, err := ve.EncodeWithType(tt, value)
log.PanicIf(err)
tagValue = NewIfdBuilderTagValueFromBytes(ed.Encoded)
}
return builderTag{
ii: ii,
tagId: tagId,
value: NewIfdBuilderTagValueFromBytes(ed.Encoded),
value: tagValue,
}
}

View File

@ -132,6 +132,11 @@ func (ibe *IfdByteEncoder) TableSize(entryCount int) uint32 {
return uint32(2) + (ibe.EntrySize() * uint32(entryCount)) + uint32(4)
}
// encodeTagToBytes encodes the given tag to a byte stream. If
// `nextIfdOffsetToWrite` is more than (0), recurse into child IFDs
// (`nextIfdOffsetToWrite` is required in order for them to know where the its
// IFD data will be written, in order for them to know the offset of where
// their allocated-data block will start, which follows right behind).
func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *builderTag, bw *ByteWriter, ida *ifdDataAllocator, nextIfdOffsetToWrite uint32) (childIfdBlock []byte, err error) {
defer func() {
if state := recover(); state != nil {
@ -194,7 +199,6 @@ func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *builderTag, bw *
log.PanicIf(err)
} else {
fourBytes := make([]byte, 4)
// TODO(dustin): We have to test that small (<4 bytes) values are aligned correctly.
copy(fourBytes, valueBytes)
err = bw.WriteFourBytes(fourBytes)
@ -212,7 +216,7 @@ func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *builderTag, bw *
if nextIfdOffsetToWrite > 0 {
var err error
// TODO(dustin): Create a tool to write the structure and allocation of the output data.
// TODO(dustin): Create a tool to print/dump the structure and allocation of the output data.
// Create the block of IFD data and everything it requires.
childIfdBlock, err = ibe.encodeAndAttachIfd(bt.value.Ib(), nextIfdOffsetToWrite)
@ -337,6 +341,10 @@ func (ibe *IfdByteEncoder) encodeAndAttachIfd(ib *IfdBuilder, ifdAddressableOffs
}
}()
if len(ib.tags) == 0 {
log.Panicf("trying to encode an IfdBuilder that doesn't have any tags")
}
// TODO(dustin): !! There's a lot of numbers-agreement required in our current implementation (where offsets are independently calculated in multiple areas, such as in the first and second runs of encodeIfdToBytes). Refactor this to share the offsets rather than repeatedly calculating them (which has a risk of fallign out of aligning and there being cosnsitency problems).
b := new(bytes.Buffer)

View File

@ -3,6 +3,7 @@ package exif
import (
"testing"
"bytes"
// "fmt"
"github.com/dsoprea/go-logging"
)
@ -293,7 +294,154 @@ func Test_IfdByteEncoder_encodeTagToBytes_bytes_allocated(t *testing.T) {
}
}
func Test_IfdByteEncoder_encodeTagToBytes_childifd__no_allocate(t *testing.T) {
ibe := NewIfdByteEncoder()
ib := &IfdBuilder{
ii: RootIi,
}
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
childIb := NewIfdBuilder(ExifIi, TestDefaultByteOrder)
bt := NewBuilderTagFromConfig(RootIi, IfdExifId, TestDefaultByteOrder, childIb)
nextIfdOffsetToWrite := uint32(0)
childIfdBlock, err := ibe.encodeTagToBytes(ib, &bt, bw, ida, nextIfdOffsetToWrite)
log.PanicIf(err)
if childIfdBlock != nil {
t.Fatalf("no child-IFDs were expected to be allocated")
} else if bytes.Compare(b.Bytes(), []byte { 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 }) != 0 {
t.Fatalf("encoded tag-entry with child-IFD not correct")
} else if ida.NextOffset() != addressableOffset {
t.Fatalf("allocation offset not expected")
}
}
func Test_IfdByteEncoder_encodeTagToBytes_childifd__with_allocate(t *testing.T) {
// Create a child IFD (represented by an IB instance) that we can allocate
// space for and then attach to a tag (which would normally be an entry,
// then, in a higher IFD).
childIb := NewIfdBuilder(ExifIi, TestDefaultByteOrder)
childIbTestTag := builderTag{
ii: ExifIi,
tagId: 0x8822,
value: NewIfdBuilderTagValueFromBytes([]byte { 0x12, 0x34 }),
}
childIb.Add(childIbTestTag)
// Formally compose the tag that refers to it.
bt := NewBuilderTagFromConfig(RootIi, IfdExifId, TestDefaultByteOrder, childIb)
// Encode the tag. Sicne we've actually provided an offset at which we can
// allocate data, the child-IFD will automatically be encoded, allocated,
// and installed into the allocated-data block (which will follow the IFD
// definition in the file, per the specfiication).
ibe := NewIfdByteEncoder()
ib := &IfdBuilder{
ii: RootIi,
}
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
// addressableOffset is the offset of where large data can be allocated
// (which follows the IFD table/block). Large, in that it can't be stored
// in the table itself. Just used for arithmetic. This is just where the
// data for the current IFD can be written. It's not absolute for the EXIF
// data in general.
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
// This is the offset of where the next IFD can be written in the EXIF byte
// stream. Just used for arithmetic.
nextIfdOffsetToWrite := uint32(2000)
childIfdBlock, err := ibe.encodeTagToBytes(ib, &bt, bw, ida, nextIfdOffsetToWrite)
log.PanicIf(err)
if ida.NextOffset() != addressableOffset {
t.Fatalf("IDA offset changed but no allocations where expected: (0x%02x)", ida.NextOffset())
}
tagBytes := b.Bytes()
if len(tagBytes) != 12 {
t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
} else if len(childIfdBlock) != 18 {
t.Fatalf("Child IFD is not the right size: (%d)", len(childIfdBlock))
}
iteV, err := ParseOneTag(RootIi, TestDefaultByteOrder, tagBytes)
log.PanicIf(err)
if iteV.TagId != IfdExifId {
t.Fatalf("IFD first tag-ID not correct: (0x%02x)", iteV.TagId)
} else if iteV.TagIndex != 0 {
t.Fatalf("IFD first tag index not correct: (%d)", iteV.TagIndex)
} else if iteV.TagType != TypeLong {
t.Fatalf("IFD first tag type not correct: (%d)", iteV.TagType)
} else if iteV.UnitCount != 1 {
t.Fatalf("IFD first tag unit-count not correct: (%d)", iteV.UnitCount)
} else if iteV.ValueOffset != nextIfdOffsetToWrite {
t.Fatalf("IFD's child-IFD offset (as offset) is not correct: (%d) != (%d)", iteV.ValueOffset, nextIfdOffsetToWrite)
} else if bytes.Compare(iteV.RawValueOffset, []byte { 0x0, 0x0, 0x07, 0xd0 }) != 0 {
t.Fatalf("IFD's child-IFD offset (as raw bytes) is not correct: [%x]", iteV.RawValueOffset)
} else if iteV.ChildIfdName != IfdExif {
t.Fatalf("IFD first tag IFD-name name not correct: [%s]", iteV.ChildIfdName)
} else if iteV.Ii != RootIi {
t.Fatalf("IFD first tag parent IFD not correct: %v", iteV.Ii)
}
// TODO(dustin): Test writing some tags that require allocation.
// Validate the child's raw IFD bytes.
childNextIfdOffset, childEntries, err := ParseOneIfd(ExifIi, TestDefaultByteOrder, childIfdBlock, nil)
log.PanicIf(err)
if childNextIfdOffset != uint32(0) {
t.Fatalf("Child IFD: Next IFD offset should be (0): (0x%08x)", childNextIfdOffset)
} else if len(childEntries) != 1 {
t.Fatalf("Child IFD: Expected exactly one entry: (%d)", len(childEntries))
}
ite := childEntries[0]
if ite.TagId != 0x8822 {
t.Fatalf("Child IFD first tag-ID not correct: (0x%02x)", ite.TagId)
} else if ite.TagIndex != 0 {
t.Fatalf("Child IFD first tag index not correct: (%d)", ite.TagIndex)
} else if ite.TagType != TypeShort {
t.Fatalf("Child IFD first tag type not correct: (%d)", ite.TagType)
} else if ite.UnitCount != 1 {
t.Fatalf("Child IFD first tag unit-count not correct: (%d)", ite.UnitCount)
} else if ite.ValueOffset != 0x12340000 {
t.Fatalf("Child IFD first tag value value (as offset) not correct: (0x%02x)", ite.ValueOffset)
} else if bytes.Compare(ite.RawValueOffset, []byte { 0x12, 0x34, 0x0, 0x0 }) != 0 {
t.Fatalf("Child IFD first tag value value (as raw bytes) not correct: [%v]", ite.RawValueOffset)
} else if ite.ChildIfdName != "" {
t.Fatalf("Child IFD first tag IFD-name name not empty: [%s]", ite.ChildIfdName)
} else if ite.Ii != ExifIi {
t.Fatalf("Child IFD first tag parent IFD not correct: %v", ite.Ii)
}
}
// TODO(dustin): !! Test all types.
// TODO(dustin): !! Test specific unknown-type tags.
// TODO(dustin): !! Test what happens with unhandled unknown-type tags (though it should never get to this point in the normal workflow).
// TODO(dustin): !! Test child IFDs (may not be possible until after writing tests for higher-level IB encode).
// TODO(dustin): !! Once we can correctly build a complete EXIF, test by trying to parse with an Exif instance.

View File

@ -153,6 +153,14 @@ func (ie *IfdEnumerate) parseTag(ii IfdIdentity, tagIndex int, ite *IfdTagEnumer
RawValueOffset: rawValueOffset,
}
// If it's an IFD but not a standard one, it'll just be seen as a LONG
// (the standard IFD tag type), later, unless we skip it because it's
// [likely] not even in the standard list of known tags.
childIfdName, isIfd := IfdTagNameWithId(ii.IfdName, tagId)
if isIfd == true {
tag.ChildIfdName = childIfdName
}
return tag, nil
}
@ -163,17 +171,13 @@ type TagVisitor func(ii IfdIdentity, ifdIndex int, tagId uint16, tagType TagType
// ParseIfd decodes the IFD block that we're currently sitting on the first
// byte of.
func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ifdOffset uint32, visitor TagVisitor, doDescend bool) (nextIfdOffset uint32, entries []IfdTagEntry, err error) {
func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ite *IfdTagEnumerator, visitor TagVisitor, doDescend bool) (nextIfdOffset uint32, entries []IfdTagEntry, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ii.IfdName, ifdIndex, ifdOffset)
ite := ie.getTagEnumerator(ifdOffset)
tagCount, _, err := ite.getUint16()
log.PanicIf(err)
@ -199,22 +203,16 @@ func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ifdOffset uint32,
log.PanicIf(err)
}
childIfdName, isIfd := IfdTagNameWithId(ii.IfdName, tag.TagId)
// If it's an IFD but not a standard one, it'll just be seen as a LONG
// (the standard IFD tag type), later, unless we skip it because it's
// [likely] not even in the standard list of known tags.
if isIfd == true {
tag.ChildIfdName = childIfdName
if tag.ChildIfdName != "" && doDescend == true {
ifdEnumerateLogger.Debugf(nil, "Descending to IFD [%s].", tag.ChildIfdName)
if doDescend == true {
ifdEnumerateLogger.Debugf(nil, "Descending to IFD [%s].", childIfdName)
childIi, _ := IfdIdOrFail(ii.IfdName, tag.ChildIfdName)
childIi, _ := IfdIdOrFail(ii.IfdName, childIfdName)
err := ie.scan(childIi, tag.ValueOffset, visitor)
log.PanicIf(err)
}
err := ie.scan(childIi, tag.ValueOffset, visitor)
log.PanicIf(err)
}
entries[i] = tag
@ -237,7 +235,10 @@ func (ie *IfdEnumerate) scan(ii IfdIdentity, ifdOffset uint32, visitor TagVisito
}()
for ifdIndex := 0;; ifdIndex++ {
nextIfdOffset, _, err := ie.ParseIfd(ii, ifdIndex, ifdOffset, visitor, true)
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ii.IfdName, ifdIndex, ifdOffset)
ite := ie.getTagEnumerator(ifdOffset)
nextIfdOffset, _, err := ie.ParseIfd(ii, ifdIndex, ite, visitor, true)
log.PanicIf(err)
if nextIfdOffset == 0 {
@ -379,7 +380,10 @@ func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error
queue = queue[1:]
nextIfdOffset, entries, err := ie.ParseIfd(ii, index, offset, nil, false)
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ii.IfdName, index, offset)
ite := ie.getTagEnumerator(offset)
nextIfdOffset, entries, err := ie.ParseIfd(ii, index, ite, nil, false)
log.PanicIf(err)
id := len(ifds)
@ -467,3 +471,44 @@ func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error
return index, nil
}
// ParseOneIfd is a hack to use an IE to parse a raw IFD block. Can be used for
// testing.
func ParseOneIfd(ii IfdIdentity, byteOrder binary.ByteOrder, ifdBlock []byte, visitor TagVisitor) (nextIfdOffset uint32, entries []IfdTagEntry, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ie := &IfdEnumerate{
byteOrder: byteOrder,
}
ite := NewIfdTagEnumerator(ifdBlock, byteOrder, 0)
nextIfdOffset, entries, err = ie.ParseIfd(ii, 0, ite, visitor, true)
log.PanicIf(err)
return nextIfdOffset, entries, nil
}
// ParseOneTag is a hack to use an IE to parse a raw tag block.
func ParseOneTag(ii IfdIdentity, byteOrder binary.ByteOrder, tagBlock []byte) (tag IfdTagEntry, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ie := &IfdEnumerate{
byteOrder: byteOrder,
}
ite := NewIfdTagEnumerator(tagBlock, byteOrder, 0)
tag, err = ie.parseTag(ii, 0, ite)
log.PanicIf(err)
return tag, nil
}