This commit is contained in:
2025-06-09 13:57:01 +08:00
commit 2bc6bd257a
89 changed files with 24207 additions and 0 deletions
Binary file not shown.
+651
View File
@@ -0,0 +1,651 @@
package exifcommon
import (
"errors"
"fmt"
"strings"
"github.com/dsoprea/go-logging"
)
var (
ifdLogger = log.NewLogger("exifcommon.ifd")
)
var (
ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
)
// MappedIfd is one node in the IFD-mapping.
type MappedIfd struct {
ParentTagId uint16
Placement []uint16
Path []string
Name string
TagId uint16
Children map[uint16]*MappedIfd
}
// String returns a descriptive string.
func (mi *MappedIfd) String() string {
pathPhrase := mi.PathPhrase()
return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
}
// PathPhrase returns a non-fully-qualified IFD path.
func (mi *MappedIfd) PathPhrase() string {
return strings.Join(mi.Path, "/")
}
// TODO(dustin): Refactor this to use IfdIdentity structs.
// IfdMapping describes all of the IFDs that we currently recognize.
type IfdMapping struct {
rootNode *MappedIfd
}
// NewIfdMapping returns a new IfdMapping struct.
func NewIfdMapping() (ifdMapping *IfdMapping) {
rootNode := &MappedIfd{
Path: make([]string, 0),
Children: make(map[uint16]*MappedIfd),
}
return &IfdMapping{
rootNode: rootNode,
}
}
// NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the
// standard IFDs.
func NewIfdMappingWithStandard() (ifdMapping *IfdMapping, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
return im, nil
}
// Get returns the node given the path slice.
func (im *IfdMapping) Get(parentPlacement []uint16) (childIfd *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ptr := im.rootNode
for _, tagId := range parentPlacement {
if descendantPtr, found := ptr.Children[tagId]; found == false {
log.Panicf("ifd child with tag-ID (%04x) not registered: [%s]", tagId, ptr.PathPhrase())
} else {
ptr = descendantPtr
}
}
return ptr, nil
}
// GetWithPath returns the node given the path string.
func (im *IfdMapping) GetWithPath(pathPhrase string) (mi *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if pathPhrase == "" {
log.Panicf("path-phrase is empty")
}
path := strings.Split(pathPhrase, "/")
ptr := im.rootNode
for _, name := range path {
var hit *MappedIfd
for _, mi := range ptr.Children {
if mi.Name == name {
hit = mi
break
}
}
if hit == nil {
log.Panicf("ifd child with name [%s] not registered: [%s]", name, ptr.PathPhrase())
}
ptr = hit
}
return ptr, nil
}
// GetChild is a convenience function to get the child path for a given parent
// placement and child tag-ID.
func (im *IfdMapping) GetChild(parentPathPhrase string, tagId uint16) (mi *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
mi, err = im.GetWithPath(parentPathPhrase)
log.PanicIf(err)
for _, childMi := range mi.Children {
if childMi.TagId == tagId {
return childMi, nil
}
}
// Whether or not an IFD is defined in data, such an IFD is not registered
// and would be unknown.
log.Panic(ErrChildIfdNotMapped)
return nil, nil
}
// IfdTagIdAndIndex represents a specific part of the IFD path.
//
// This is a legacy type.
type IfdTagIdAndIndex struct {
Name string
TagId uint16
Index int
}
// String returns a descriptive string.
func (itii IfdTagIdAndIndex) String() string {
return fmt.Sprintf("IfdTagIdAndIndex<NAME=[%s] ID=(%04x) INDEX=(%d)>", itii.Name, itii.TagId, itii.Index)
}
// ResolvePath takes a list of names, which can also be suffixed with indices
// (to identify the second, third, etc.. sibling IFD) and returns a list of
// tag-IDs and those indices.
//
// Example:
//
// - IFD/Exif/Iop
// - IFD0/Exif/Iop
//
// This is the only call that supports adding the numeric indices.
func (im *IfdMapping) ResolvePath(pathPhrase string) (lineage []IfdTagIdAndIndex, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
pathPhrase = strings.TrimSpace(pathPhrase)
if pathPhrase == "" {
log.Panicf("can not resolve empty path-phrase")
}
path := strings.Split(pathPhrase, "/")
lineage = make([]IfdTagIdAndIndex, len(path))
ptr := im.rootNode
empty := IfdTagIdAndIndex{}
for i, name := range path {
indexByte := name[len(name)-1]
index := 0
if indexByte >= '0' && indexByte <= '9' {
index = int(indexByte - '0')
name = name[:len(name)-1]
}
itii := IfdTagIdAndIndex{}
for _, mi := range ptr.Children {
if mi.Name != name {
continue
}
itii.Name = name
itii.TagId = mi.TagId
itii.Index = index
ptr = mi
break
}
if itii == empty {
log.Panicf("ifd child with name [%s] not registered: [%s]", name, pathPhrase)
}
lineage[i] = itii
}
return lineage, nil
}
// FqPathPhraseFromLineage returns the fully-qualified IFD path from the slice.
func (im *IfdMapping) FqPathPhraseFromLineage(lineage []IfdTagIdAndIndex) (fqPathPhrase string) {
fqPathParts := make([]string, len(lineage))
for i, itii := range lineage {
if itii.Index > 0 {
fqPathParts[i] = fmt.Sprintf("%s%d", itii.Name, itii.Index)
} else {
fqPathParts[i] = itii.Name
}
}
return strings.Join(fqPathParts, "/")
}
// PathPhraseFromLineage returns the non-fully-qualified IFD path from the
// slice.
func (im *IfdMapping) PathPhraseFromLineage(lineage []IfdTagIdAndIndex) (pathPhrase string) {
pathParts := make([]string, len(lineage))
for i, itii := range lineage {
pathParts[i] = itii.Name
}
return strings.Join(pathParts, "/")
}
// StripPathPhraseIndices returns a non-fully-qualified path-phrase (no
// indices).
func (im *IfdMapping) StripPathPhraseIndices(pathPhrase string) (strippedPathPhrase string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
lineage, err := im.ResolvePath(pathPhrase)
log.PanicIf(err)
strippedPathPhrase = im.PathPhraseFromLineage(lineage)
return strippedPathPhrase, nil
}
// Add puts the given IFD at the given position of the tree. The position of the
// tree is referred to as the placement and is represented by a set of tag-IDs,
// where the leftmost is the root tag and the tags going to the right are
// progressive descendants.
func (im *IfdMapping) Add(parentPlacement []uint16, tagId uint16, name string) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! It would be nicer to provide a list of names in the placement rather than tag-IDs.
ptr, err := im.Get(parentPlacement)
log.PanicIf(err)
path := make([]string, len(parentPlacement)+1)
if len(parentPlacement) > 0 {
copy(path, ptr.Path)
}
path[len(path)-1] = name
placement := make([]uint16, len(parentPlacement)+1)
if len(placement) > 0 {
copy(placement, ptr.Placement)
}
placement[len(placement)-1] = tagId
childIfd := &MappedIfd{
ParentTagId: ptr.TagId,
Path: path,
Placement: placement,
Name: name,
TagId: tagId,
Children: make(map[uint16]*MappedIfd),
}
if _, found := ptr.Children[tagId]; found == true {
log.Panicf("child IFD with tag-ID (%04x) already registered under IFD [%s] with tag-ID (%04x)", tagId, ptr.Name, ptr.TagId)
}
ptr.Children[tagId] = childIfd
return nil
}
func (im *IfdMapping) dumpLineages(stack []*MappedIfd, input []string) (output []string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
currentIfd := stack[len(stack)-1]
output = input
for _, childIfd := range currentIfd.Children {
stackCopy := make([]*MappedIfd, len(stack)+1)
copy(stackCopy, stack)
stackCopy[len(stack)] = childIfd
// Add to output, but don't include the obligatory root node.
parts := make([]string, len(stackCopy)-1)
for i, mi := range stackCopy[1:] {
parts[i] = mi.Name
}
output = append(output, strings.Join(parts, "/"))
output, err = im.dumpLineages(stackCopy, output)
log.PanicIf(err)
}
return output, nil
}
// DumpLineages returns a slice of strings representing all mappings.
func (im *IfdMapping) DumpLineages() (output []string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
stack := []*MappedIfd{im.rootNode}
output = make([]string, 0)
output, err = im.dumpLineages(stack, output)
log.PanicIf(err)
return output, nil
}
// LoadStandardIfds loads the standard IFDs into the mapping.
func LoadStandardIfds(im *IfdMapping) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = im.Add(
[]uint16{},
IfdStandardIfdIdentity.TagId(), IfdStandardIfdIdentity.Name())
log.PanicIf(err)
err = im.Add(
[]uint16{IfdStandardIfdIdentity.TagId()},
IfdExifStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.Name())
log.PanicIf(err)
err = im.Add(
[]uint16{IfdStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.TagId()},
IfdExifIopStandardIfdIdentity.TagId(), IfdExifIopStandardIfdIdentity.Name())
log.PanicIf(err)
err = im.Add(
[]uint16{IfdStandardIfdIdentity.TagId()},
IfdGpsInfoStandardIfdIdentity.TagId(), IfdGpsInfoStandardIfdIdentity.Name())
log.PanicIf(err)
return nil
}
// IfdTag describes a single IFD tag and its parent (if any).
type IfdTag struct {
parentIfdTag *IfdTag
tagId uint16
name string
}
func NewIfdTag(parentIfdTag *IfdTag, tagId uint16, name string) IfdTag {
return IfdTag{
parentIfdTag: parentIfdTag,
tagId: tagId,
name: name,
}
}
// ParentIfd returns the IfdTag of this IFD's parent.
func (it IfdTag) ParentIfd() *IfdTag {
return it.parentIfdTag
}
// TagId returns the tag-ID of this IFD.
func (it IfdTag) TagId() uint16 {
return it.tagId
}
// Name returns the simple name of this IFD.
func (it IfdTag) Name() string {
return it.name
}
// String returns a descriptive string.
func (it IfdTag) String() string {
parentIfdPhrase := ""
if it.parentIfdTag != nil {
parentIfdPhrase = fmt.Sprintf(" PARENT=(0x%04x)[%s]", it.parentIfdTag.tagId, it.parentIfdTag.name)
}
return fmt.Sprintf("IfdTag<TAG-ID=(0x%04x) NAME=[%s]%s>", it.tagId, it.name, parentIfdPhrase)
}
var (
// rootStandardIfd is the standard root IFD.
rootStandardIfd = NewIfdTag(nil, 0x0000, "IFD") // IFD
// exifStandardIfd is the standard "Exif" IFD.
exifStandardIfd = NewIfdTag(&rootStandardIfd, 0x8769, "Exif") // IFD/Exif
// iopStandardIfd is the standard "Iop" IFD.
iopStandardIfd = NewIfdTag(&exifStandardIfd, 0xA005, "Iop") // IFD/Exif/Iop
// gpsInfoStandardIfd is the standard "GPS" IFD.
gpsInfoStandardIfd = NewIfdTag(&rootStandardIfd, 0x8825, "GPSInfo") // IFD/GPSInfo
)
// IfdIdentityPart represents one component in an IFD path.
type IfdIdentityPart struct {
Name string
Index int
}
// String returns a fully-qualified IFD path.
func (iip IfdIdentityPart) String() string {
if iip.Index > 0 {
return fmt.Sprintf("%s%d", iip.Name, iip.Index)
} else {
return iip.Name
}
}
// UnindexedString returned a non-fully-qualified IFD path.
func (iip IfdIdentityPart) UnindexedString() string {
return iip.Name
}
// IfdIdentity represents a single IFD path and provides access to various
// information and representations.
//
// Only global instances can be used for equality checks.
type IfdIdentity struct {
ifdTag IfdTag
parts []IfdIdentityPart
ifdPath string
fqIfdPath string
}
// NewIfdIdentity returns a new IfdIdentity struct.
func NewIfdIdentity(ifdTag IfdTag, parts ...IfdIdentityPart) (ii *IfdIdentity) {
ii = &IfdIdentity{
ifdTag: ifdTag,
parts: parts,
}
ii.ifdPath = ii.getIfdPath()
ii.fqIfdPath = ii.getFqIfdPath()
return ii
}
// NewIfdIdentityFromString parses a string like "IFD/Exif" or "IFD1" or
// something more exotic with custom IFDs ("SomeIFD4/SomeChildIFD6"). Note that
// this will valid the unindexed IFD structure (because the standard tags from
// the specification are unindexed), but not, obviously, any indices (e.g.
// the numbers in "IFD0", "IFD1", "SomeIFD4/SomeChildIFD6"). It is
// required for the caller to check whether these specific instances
// were actually parsed out of the stream.
func NewIfdIdentityFromString(im *IfdMapping, fqIfdPath string) (ii *IfdIdentity, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
lineage, err := im.ResolvePath(fqIfdPath)
log.PanicIf(err)
var lastIt *IfdTag
identityParts := make([]IfdIdentityPart, len(lineage))
for i, itii := range lineage {
// Build out the tag that will eventually point to the IFD represented
// by the right-most part in the IFD path.
it := &IfdTag{
parentIfdTag: lastIt,
tagId: itii.TagId,
name: itii.Name,
}
lastIt = it
// Create the next IfdIdentity part.
iip := IfdIdentityPart{
Name: itii.Name,
Index: itii.Index,
}
identityParts[i] = iip
}
ii = NewIfdIdentity(*lastIt, identityParts...)
return ii, nil
}
func (ii *IfdIdentity) getFqIfdPath() string {
partPhrases := make([]string, len(ii.parts))
for i, iip := range ii.parts {
partPhrases[i] = iip.String()
}
return strings.Join(partPhrases, "/")
}
func (ii *IfdIdentity) getIfdPath() string {
partPhrases := make([]string, len(ii.parts))
for i, iip := range ii.parts {
partPhrases[i] = iip.UnindexedString()
}
return strings.Join(partPhrases, "/")
}
// String returns a fully-qualified IFD path.
func (ii *IfdIdentity) String() string {
return ii.fqIfdPath
}
// UnindexedString returns a non-fully-qualified IFD path.
func (ii *IfdIdentity) UnindexedString() string {
return ii.ifdPath
}
// IfdTag returns the tag struct behind this IFD.
func (ii *IfdIdentity) IfdTag() IfdTag {
return ii.ifdTag
}
// TagId returns the tag-ID of the IFD.
func (ii *IfdIdentity) TagId() uint16 {
return ii.ifdTag.TagId()
}
// LeafPathPart returns the last right-most path-part, which represents the
// current IFD.
func (ii *IfdIdentity) LeafPathPart() IfdIdentityPart {
return ii.parts[len(ii.parts)-1]
}
// Name returns the simple name of this IFD.
func (ii *IfdIdentity) Name() string {
return ii.LeafPathPart().Name
}
// Index returns the index of this IFD (more then one IFD under a parent IFD
// will be numbered [0..n]).
func (ii *IfdIdentity) Index() int {
return ii.LeafPathPart().Index
}
// Equals returns true if the two IfdIdentity instances are effectively
// identical.
//
// Since there's no way to get a specific fully-qualified IFD path without a
// certain slice of parts and all other fields are also derived from this,
// checking that the fully-qualified IFD path is equals is sufficient.
func (ii *IfdIdentity) Equals(ii2 *IfdIdentity) bool {
return ii.String() == ii2.String()
}
// NewChild creates an IfdIdentity for an IFD that is a child of the current
// IFD.
func (ii *IfdIdentity) NewChild(childIfdTag IfdTag, index int) (iiChild *IfdIdentity) {
if *childIfdTag.parentIfdTag != ii.ifdTag {
log.Panicf("can not add child; we are not the parent:\nUS=%v\nCHILD=%v", ii.ifdTag, childIfdTag)
}
childPart := IfdIdentityPart{childIfdTag.name, index}
childParts := append(ii.parts, childPart)
iiChild = NewIfdIdentity(childIfdTag, childParts...)
return iiChild
}
// NewSibling creates an IfdIdentity for an IFD that is a sibling to the current
// one.
func (ii *IfdIdentity) NewSibling(index int) (iiSibling *IfdIdentity) {
parts := make([]IfdIdentityPart, len(ii.parts))
copy(parts, ii.parts)
parts[len(parts)-1].Index = index
iiSibling = NewIfdIdentity(ii.ifdTag, parts...)
return iiSibling
}
var (
// IfdStandardIfdIdentity represents the IFD path for IFD0.
IfdStandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 0})
// IfdExifStandardIfdIdentity represents the IFD path for IFD0/Exif0.
IfdExifStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(exifStandardIfd, 0)
// IfdExifIopStandardIfdIdentity represents the IFD path for IFD0/Exif0/Iop0.
IfdExifIopStandardIfdIdentity = IfdExifStandardIfdIdentity.NewChild(iopStandardIfd, 0)
// IfdGPSInfoStandardIfdIdentity represents the IFD path for IFD0/GPSInfo0.
IfdGpsInfoStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(gpsInfoStandardIfd, 0)
// Ifd1StandardIfdIdentity represents the IFD path for IFD1.
Ifd1StandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 1})
)
+336
View File
@@ -0,0 +1,336 @@
package exifcommon
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/dsoprea/go-logging"
)
func TestIfdMapping_Add(t *testing.T) {
im := NewIfdMapping()
err := im.Add([]uint16{}, 0x1111, "ifd0")
log.PanicIf(err)
err = im.Add([]uint16{0x1111}, 0x4444, "ifd00")
log.PanicIf(err)
err = im.Add([]uint16{0x1111, 0x4444}, 0x5555, "ifd000")
log.PanicIf(err)
err = im.Add([]uint16{}, 0x2222, "ifd1")
log.PanicIf(err)
err = im.Add([]uint16{}, 0x3333, "ifd2")
log.PanicIf(err)
lineages, err := im.DumpLineages()
log.PanicIf(err)
sort.Strings(lineages)
expected := []string{
"ifd0",
"ifd0/ifd00",
"ifd0/ifd00/ifd000",
"ifd1",
"ifd2",
}
if reflect.DeepEqual(lineages, expected) != true {
fmt.Printf("Actual:\n")
fmt.Printf("\n")
for i, line := range lineages {
fmt.Printf("(%d) %s\n", i, line)
}
fmt.Printf("\n")
fmt.Printf("Expected:\n")
fmt.Printf("\n")
for i, line := range expected {
fmt.Printf("(%d) %s\n", i, line)
}
t.Fatalf("IFD-mapping dump not correct.")
}
}
func TestIfdMapping_LoadStandardIfds(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
lineages, err := im.DumpLineages()
log.PanicIf(err)
sort.Strings(lineages)
expected := []string{
"IFD",
"IFD/Exif",
"IFD/Exif/Iop",
"IFD/GPSInfo",
}
if reflect.DeepEqual(lineages, expected) != true {
fmt.Printf("Actual:\n")
fmt.Printf("\n")
for i, line := range lineages {
fmt.Printf("(%d) %s\n", i, line)
}
fmt.Printf("\n")
fmt.Printf("Expected:\n")
fmt.Printf("\n")
for i, line := range expected {
fmt.Printf("(%d) %s\n", i, line)
}
t.Fatalf("IFD-mapping dump not correct.")
}
}
func TestIfdMapping_Get(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
mi, err := im.Get([]uint16{
IfdStandardIfdIdentity.TagId(),
IfdExifStandardIfdIdentity.TagId(),
IfdExifIopStandardIfdIdentity.TagId(),
})
log.PanicIf(err)
if mi.ParentTagId != IfdExifStandardIfdIdentity.TagId() {
t.Fatalf("Parent tag-ID not correct")
} else if mi.TagId != IfdExifIopStandardIfdIdentity.TagId() {
t.Fatalf("Tag-ID not correct")
} else if mi.Name != "Iop" {
t.Fatalf("name not correct")
} else if mi.PathPhrase() != "IFD/Exif/Iop" {
t.Fatalf("path not correct")
}
}
func TestIfdMapping_GetWithPath(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
mi, err := im.GetWithPath("IFD/Exif/Iop")
log.PanicIf(err)
if mi.ParentTagId != IfdExifStandardIfdIdentity.TagId() {
t.Fatalf("Parent tag-ID not correct")
} else if mi.TagId != IfdExifIopStandardIfdIdentity.TagId() {
t.Fatalf("Tag-ID not correct")
} else if mi.Name != "Iop" {
t.Fatalf("name not correct")
} else if mi.PathPhrase() != "IFD/Exif/Iop" {
t.Fatalf("path not correct")
}
}
func TestIfdMapping_ResolvePath__Regular(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
lineage, err := im.ResolvePath("IFD/Exif/Iop")
log.PanicIf(err)
expected := []IfdTagIdAndIndex{
{Name: "IFD", TagId: 0, Index: 0},
{Name: "Exif", TagId: 0x8769, Index: 0},
{Name: "Iop", TagId: 0xa005, Index: 0},
}
if reflect.DeepEqual(lineage, expected) != true {
t.Fatalf("Lineage not correct.")
}
}
func TestIfdMapping_ResolvePath__WithIndices(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
lineage, err := im.ResolvePath("IFD/Exif1/Iop")
log.PanicIf(err)
expected := []IfdTagIdAndIndex{
{Name: "IFD", TagId: 0, Index: 0},
{Name: "Exif", TagId: 0x8769, Index: 1},
{Name: "Iop", TagId: 0xa005, Index: 0},
}
if reflect.DeepEqual(lineage, expected) != true {
t.Fatalf("Lineage not correct.")
}
}
func TestIfdMapping_ResolvePath__Miss(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
_, err = im.ResolvePath("IFD/Exif/Invalid")
if err == nil {
t.Fatalf("Expected failure for invalid IFD path.")
} else if err.Error() != "ifd child with name [Invalid] not registered: [IFD/Exif/Invalid]" {
log.Panic(err)
}
}
func TestIfdMapping_FqPathPhraseFromLineage(t *testing.T) {
lineage := []IfdTagIdAndIndex{
{Name: "IFD", Index: 0},
{Name: "Exif", Index: 1},
{Name: "Iop", Index: 0},
}
im := NewIfdMapping()
fqPathPhrase := im.FqPathPhraseFromLineage(lineage)
if fqPathPhrase != "IFD/Exif1/Iop" {
t.Fatalf("path-phrase not correct: [%s]", fqPathPhrase)
}
}
func TestIfdMapping_PathPhraseFromLineage(t *testing.T) {
lineage := []IfdTagIdAndIndex{
{Name: "IFD", Index: 0},
{Name: "Exif", Index: 1},
{Name: "Iop", Index: 0},
}
im := NewIfdMapping()
fqPathPhrase := im.PathPhraseFromLineage(lineage)
if fqPathPhrase != "IFD/Exif/Iop" {
t.Fatalf("path-phrase not correct: [%s]", fqPathPhrase)
}
}
func TestIfdMapping_NewIfdMappingWithStandard(t *testing.T) {
imWith, err := NewIfdMappingWithStandard()
log.PanicIf(err)
imWithout := NewIfdMapping()
err = LoadStandardIfds(imWithout)
log.PanicIf(err)
outputWith, err := imWith.DumpLineages()
log.PanicIf(err)
sort.Strings(outputWith)
outputWithout, err := imWithout.DumpLineages()
log.PanicIf(err)
sort.Strings(outputWithout)
if reflect.DeepEqual(outputWith, outputWithout) != true {
fmt.Printf("WITH:\n")
fmt.Printf("\n")
for _, line := range outputWith {
fmt.Printf("%s\n", line)
}
fmt.Printf("\n")
fmt.Printf("WITHOUT:\n")
fmt.Printf("\n")
for _, line := range outputWithout {
fmt.Printf("%s\n", line)
}
fmt.Printf("\n")
t.Fatalf("Standard IFDs not loaded correctly.")
}
}
func TestNewIfdIdentityFromString_Valid_WithoutIndexes(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
fqIfdPath := "IFD/Exif"
ii, err := NewIfdIdentityFromString(im, fqIfdPath)
log.PanicIf(err)
if ii.String() != fqIfdPath {
t.Fatalf("'%s' IFD-path was not parsed correctly: [%s]", fqIfdPath, ii.String())
}
}
func TestNewIfdIdentityFromString_Valid_WithIndexes(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
fqIfdPath := "IFD2/Exif4"
ii, err := NewIfdIdentityFromString(im, fqIfdPath)
log.PanicIf(err)
if ii.String() != fqIfdPath {
t.Fatalf("'%s' IFD-path was not parsed correctly: [%s]", fqIfdPath, ii.String())
}
}
func TestNewIfdIdentityFromString_Invalid_IfdPathJustRoot(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
fqIfdPath := "XYZ"
_, err = NewIfdIdentityFromString(im, fqIfdPath)
if err == nil {
t.Fatalf("Expected error from invalid path.")
} else if err.Error() != "ifd child with name [XYZ] not registered: [XYZ]" {
log.Panic(err)
}
}
func TestNewIfdIdentityFromString_Invalid_IfdPathWithSubdirectory(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
fqIfdPath := "IFD/XYZ"
_, err = NewIfdIdentityFromString(im, fqIfdPath)
if err == nil {
t.Fatalf("Expected error from invalid path.")
} else if err.Error() != "ifd child with name [XYZ] not registered: [IFD/XYZ]" {
log.Panic(err)
}
}
+280
View File
@@ -0,0 +1,280 @@
package exifcommon
import (
"bytes"
"errors"
"math"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
parserLogger = log.NewLogger("exifcommon.parser")
)
var (
ErrParseFail = errors.New("parse failure")
)
// Parser knows how to parse all well-defined, encoded EXIF types.
type Parser struct {
}
// ParseBytesknows how to parse a byte-type value.
func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Add test
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 that
// should be at the end of the encoding.
func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Add test
count := int(unitCount)
if len(data) < (TypeAscii.Size() * count) {
log.Panic(ErrNotEnoughData)
}
if len(data) == 0 || data[count-1] != 0 {
s := string(data[:count])
parserLogger.Warningf(nil, "ASCII not terminated with NUL as expected: [%v]", s)
for i, c := range s {
if c > 127 {
// Binary
t := s[:i]
parserLogger.Warningf(nil, "ASCII also had binary characters. Truncating: [%v]->[%s]", s, t)
return t, nil
}
}
return s, nil
}
// 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))
}
}()
// TODO(dustin): Add test
count := int(unitCount)
if len(data) < (TypeAscii.Size() * count) {
log.Panic(ErrNotEnoughData)
}
return string(data[:count]), nil
}
// ParseShorts knows how to parse an encoded list of shorts.
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))
}
}()
// TODO(dustin): Add test
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
}
// ParseLongs knows how to encode an encoded list of unsigned longs.
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))
}
}()
// TODO(dustin): Add test
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
}
// ParseFloats knows how to encode an encoded list of floats.
func (p *Parser) ParseFloats(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []float32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) != (TypeFloat.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]float32, count)
for i := 0; i < count; i++ {
value[i] = math.Float32frombits(byteOrder.Uint32(data[i*4 : (i+1)*4]))
}
return value, nil
}
// ParseDoubles knows how to encode an encoded list of doubles.
func (p *Parser) ParseDoubles(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []float64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) != (TypeDouble.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]float64, count)
for i := 0; i < count; i++ {
value[i] = math.Float64frombits(byteOrder.Uint64(data[i*8 : (i+1)*8]))
}
return value, nil
}
// ParseRationals knows how to parse an encoded list of unsigned rationals.
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))
}
}()
// TODO(dustin): Add test
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
}
// ParseSignedLongs knows how to parse an encoded list of signed longs.
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))
}
}()
// TODO(dustin): Add test
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
}
// ParseSignedRationals knows how to parse an encoded list of signed
// rationals.
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))
}
}()
// TODO(dustin): Add test
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
}
+393
View File
@@ -0,0 +1,393 @@
package exifcommon
import (
"bytes"
"math"
"reflect"
"testing"
"github.com/dsoprea/go-logging"
)
func TestParser_ParseBytes(t *testing.T) {
p := new(Parser)
encoded := []byte("abcdefg")
value, err := p.ParseBytes(encoded, 1)
log.PanicIf(err)
if bytes.Equal(value, encoded[:1]) != true {
t.Fatalf("Encoding not correct (1): %v", value)
}
value, err = p.ParseBytes(encoded, 4)
log.PanicIf(err)
if bytes.Equal(value, encoded[:4]) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
value, err = p.ParseBytes(encoded, uint32(len(encoded)))
log.PanicIf(err)
if bytes.Equal(value, encoded) != true {
t.Fatalf("Encoding not correct (3): %v", value)
}
}
func TestParser_ParseAscii(t *testing.T) {
p := new(Parser)
original := "abcdefg"
encoded := []byte(original[:1])
encoded = append(encoded, 0)
value, err := p.ParseAscii(encoded, uint32(len(encoded)))
log.PanicIf(err)
if value != original[:1] {
t.Fatalf("Encoding not correct (1): %s", value)
}
encoded = []byte(original[:4])
encoded = append(encoded, 0)
value, err = p.ParseAscii(encoded, uint32(len(encoded)))
log.PanicIf(err)
if value != original[:4] {
t.Fatalf("Encoding not correct (2): %v", value)
}
encoded = []byte(original)
encoded = append(encoded, 0)
value, err = p.ParseAscii(encoded, uint32(len(encoded)))
log.PanicIf(err)
if value != original {
t.Fatalf("Encoding not correct (3): %v", value)
}
}
func TestParser_ParseAsciiNoNul(t *testing.T) {
p := new(Parser)
original := "abcdefg"
encoded := []byte(original)
value, err := p.ParseAsciiNoNul(encoded, 1)
log.PanicIf(err)
if value != original[:1] {
t.Fatalf("Encoding not correct (1): %s", value)
}
value, err = p.ParseAsciiNoNul(encoded, 4)
log.PanicIf(err)
if value != original[:4] {
t.Fatalf("Encoding not correct (2): %v", value)
}
value, err = p.ParseAsciiNoNul(encoded, uint32(len(encoded)))
log.PanicIf(err)
if value != original {
t.Fatalf("Encoding not correct (3): (%d) %v", len(value), value)
}
}
func TestParser_ParseShorts__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{0x00, 0x01}
value, err := p.ParseShorts(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []uint16{1}) != true {
t.Fatalf("Encoding not correct (1): %v", value)
}
encoded = []byte{0x00, 0x01, 0x00, 0x02}
value, err = p.ParseShorts(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []uint16{1}) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
func TestParser_ParseShorts__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{0x00, 0x01, 0x00, 0x02}
value, err := p.ParseShorts(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []uint16{1, 2}) != true {
t.Fatalf("Encoding not correct: %v", value)
}
}
func TestParser_ParseLongs__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{0x00, 0x00, 0x00, 0x01}
value, err := p.ParseLongs(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []uint32{1}) != true {
t.Fatalf("Encoding not correct (1): %v", value)
}
encoded = []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
value, err = p.ParseLongs(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []uint32{1}) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
func TestParser_ParseLongs__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
value, err := p.ParseLongs(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []uint32{1, 2}) != true {
t.Fatalf("Encoding not correct: %v", value)
}
}
func TestParser_ParseFloats__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{0x40, 0x49, 0x0f, 0xdb}
value, err := p.ParseFloats(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
expectedResult := []float32{3.14159265}
for i, v := range value {
if v < expectedResult[i] ||
v >= math.Nextafter32(expectedResult[i], expectedResult[i]+1) {
t.Fatalf("Encoding not correct (1): %v", value)
}
}
}
func TestParser_ParseFloats__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{0x40, 0x49, 0x0f, 0xdb, 0x40, 0x2d, 0xf8, 0x54}
value, err := p.ParseFloats(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
expectedResult := []float32{3.14159265, 2.71828182}
for i, v := range value {
if v < expectedResult[i] ||
v >= math.Nextafter32(expectedResult[i], expectedResult[i]+1) {
t.Fatalf("Encoding not correct (1): %v", value)
}
}
}
func TestParser_ParseDoubles__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1}
value, err := p.ParseDoubles(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
expectedResult := []float64{3.14159265}
for i, v := range value {
if v < expectedResult[i] ||
v >= math.Nextafter(expectedResult[i], expectedResult[i]+1) {
t.Fatalf("Encoding not correct (1): %v", value)
}
}
}
func TestParser_ParseDoubles__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1,
0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd}
value, err := p.ParseDoubles(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
expectedResult := []float64{3.14159265, 2.71828182}
for i, v := range value {
if v < expectedResult[i] ||
v >= math.Nextafter(expectedResult[i], expectedResult[i]+1) {
t.Fatalf("Encoding not correct: %v", value)
}
}
}
func TestParser_ParseRationals__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
}
value, err := p.ParseRationals(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
expected := []Rational{
{Numerator: 1, Denominator: 2},
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("Encoding not correct (1): %v", value)
}
encoded = []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
}
value, err = p.ParseRationals(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
expected = []Rational{
{Numerator: 1, Denominator: 2},
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
func TestParser_ParseRationals__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
}
value, err := p.ParseRationals(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
expected := []Rational{
{Numerator: 1, Denominator: 2},
{Numerator: 3, Denominator: 4},
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
func TestParser_ParseSignedLongs__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{0x00, 0x00, 0x00, 0x01}
value, err := p.ParseSignedLongs(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []int32{1}) != true {
t.Fatalf("Encoding not correct (1): %v", value)
}
encoded = []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
value, err = p.ParseSignedLongs(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []int32{1}) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
func TestParser_ParseSignedLongs__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
value, err := p.ParseSignedLongs(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, []int32{1, 2}) != true {
t.Fatalf("Encoding not correct: %v", value)
}
}
func TestParser_ParseSignedRationals__Single(t *testing.T) {
p := new(Parser)
encoded := []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
}
value, err := p.ParseSignedRationals(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
expected := []SignedRational{
{Numerator: 1, Denominator: 2},
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("Encoding not correct (1): %v", value)
}
encoded = []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
}
value, err = p.ParseSignedRationals(encoded, 1, TestDefaultByteOrder)
log.PanicIf(err)
expected = []SignedRational{
{Numerator: 1, Denominator: 2},
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
func TestParser_ParseSignedRationals__Multiple(t *testing.T) {
p := new(Parser)
encoded := []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
}
value, err := p.ParseSignedRationals(encoded, 2, TestDefaultByteOrder)
log.PanicIf(err)
expected := []SignedRational{
{Numerator: 1, Denominator: 2},
{Numerator: 3, Denominator: 4},
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("Encoding not correct (2): %v", value)
}
}
+88
View File
@@ -0,0 +1,88 @@
package exifcommon
import (
"os"
"path"
"encoding/binary"
"io/ioutil"
"github.com/dsoprea/go-logging"
)
var (
moduleRootPath = ""
testExifData []byte = nil
// EncodeDefaultByteOrder is the default byte-order for encoding operations.
EncodeDefaultByteOrder = binary.BigEndian
// Default byte order for tests.
TestDefaultByteOrder = binary.BigEndian
)
func GetModuleRootPath() string {
if moduleRootPath == "" {
moduleRootPath = os.Getenv("EXIF_MODULE_ROOT_PATH")
if moduleRootPath != "" {
return moduleRootPath
}
currentWd, err := os.Getwd()
log.PanicIf(err)
currentPath := currentWd
visited := make([]string, 0)
for {
tryStampFilepath := path.Join(currentPath, ".MODULE_ROOT")
_, err := os.Stat(tryStampFilepath)
if err != nil && os.IsNotExist(err) != true {
log.Panic(err)
} else if err == nil {
break
}
visited = append(visited, tryStampFilepath)
currentPath = path.Dir(currentPath)
if currentPath == "/" {
log.Panicf("could not find module-root: %v", visited)
}
}
moduleRootPath = currentPath
}
return moduleRootPath
}
func GetTestAssetsPath() string {
moduleRootPath := GetModuleRootPath()
assetsPath := path.Join(moduleRootPath, "assets")
return assetsPath
}
func getTestImageFilepath() string {
assetsPath := GetTestAssetsPath()
testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg")
return testImageFilepath
}
func getTestExifData() []byte {
if testExifData == nil {
assetsPath := GetTestAssetsPath()
filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
var err error
testExifData, err = ioutil.ReadFile(filepath)
log.PanicIf(err)
}
return testExifData
}
+482
View File
@@ -0,0 +1,482 @@
package exifcommon
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"unicode"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
typeLogger = log.NewLogger("exif.type")
)
var (
// ErrNotEnoughData is used when there isn't enough data to accommodate what
// we're trying to parse (sizeof(type) * unit_count).
ErrNotEnoughData = errors.New("not enough data for type")
// ErrWrongType is used when we try to parse anything other than the
// current type.
ErrWrongType = errors.New("wrong type, can not parse")
// ErrUnhandledUndefinedTypedTag is used when we try to parse a tag that's
// recorded as an "unknown" type but not a documented tag (therefore
// leaving us not knowning how to read it).
ErrUnhandledUndefinedTypedTag = errors.New("not a standard unknown-typed tag")
)
// TagTypePrimitive is a type-alias that let's us easily lookup type properties.
type TagTypePrimitive uint16
const (
// TypeByte describes an encoded list of bytes.
TypeByte TagTypePrimitive = 1
// TypeAscii describes an encoded list of characters that is terminated
// with a NUL in its encoded form.
TypeAscii TagTypePrimitive = 2
// TypeShort describes an encoded list of shorts.
TypeShort TagTypePrimitive = 3
// TypeLong describes an encoded list of longs.
TypeLong TagTypePrimitive = 4
// TypeRational describes an encoded list of rationals.
TypeRational TagTypePrimitive = 5
// TypeUndefined describes an encoded value that has a complex/non-clearcut
// interpretation.
TypeUndefined TagTypePrimitive = 7
// We've seen type-8, but have no documentation on it.
// TypeSignedLong describes an encoded list of signed longs.
TypeSignedLong TagTypePrimitive = 9
// TypeSignedRational describes an encoded list of signed rationals.
TypeSignedRational TagTypePrimitive = 10
// TypeFloat describes an encoded list of floats
TypeFloat TagTypePrimitive = 11
// TypeDouble describes an encoded list of doubles.
TypeDouble TagTypePrimitive = 12
// TypeAsciiNoNul is just a pseudo-type, for our own purposes.
TypeAsciiNoNul TagTypePrimitive = 0xf0
)
// String returns the name of the type
func (typeType TagTypePrimitive) String() string {
return TypeNames[typeType]
}
// Size returns the size of one atomic unit of the type.
func (tagType TagTypePrimitive) Size() int {
switch tagType {
case TypeByte, TypeAscii, TypeAsciiNoNul:
return 1
case TypeShort:
return 2
case TypeLong, TypeSignedLong, TypeFloat:
return 4
case TypeRational, TypeSignedRational, TypeDouble:
return 8
default:
log.Panicf("can not determine tag-value size for type (%d): [%s]",
tagType,
TypeNames[tagType])
// Never called.
return 0
}
}
// IsValid returns true if tagType is a valid type.
func (tagType TagTypePrimitive) IsValid() bool {
// TODO(dustin): Add test
return tagType == TypeByte ||
tagType == TypeAscii ||
tagType == TypeAsciiNoNul ||
tagType == TypeShort ||
tagType == TypeLong ||
tagType == TypeRational ||
tagType == TypeSignedLong ||
tagType == TypeSignedRational ||
tagType == TypeFloat ||
tagType == TypeDouble ||
tagType == TypeUndefined
}
var (
// TODO(dustin): Rename TypeNames() to typeNames() and add getter.
TypeNames = map[TagTypePrimitive]string{
TypeByte: "BYTE",
TypeAscii: "ASCII",
TypeShort: "SHORT",
TypeLong: "LONG",
TypeRational: "RATIONAL",
TypeUndefined: "UNDEFINED",
TypeSignedLong: "SLONG",
TypeSignedRational: "SRATIONAL",
TypeFloat: "FLOAT",
TypeDouble: "DOUBLE",
TypeAsciiNoNul: "_ASCII_NO_NUL",
}
typeNamesR = map[string]TagTypePrimitive{}
)
// Rational describes an unsigned rational value.
type Rational struct {
// Numerator is the numerator of the rational value.
Numerator uint32
// Denominator is the numerator of the rational value.
Denominator uint32
}
// SignedRational describes a signed rational value.
type SignedRational struct {
// Numerator is the numerator of the rational value.
Numerator int32
// Denominator is the numerator of the rational value.
Denominator int32
}
func isPrintableText(s string) bool {
for _, c := range s {
// unicode.IsPrint() returns false for newline characters.
if c == 0x0d || c == 0x0a {
continue
} else if unicode.IsPrint(rune(c)) == false {
return false
}
}
return true
}
// Format returns a stringified value for the given encoding. Automatically
// parses. Automatically calculates count based on type size. This function
// also supports undefined-type values (the ones that we support, anyway) by
// way of the String() method that they all require. We can't be more specific
// because we're a base package and we can't refer to it.
func FormatFromType(value interface{}, justFirst bool) (phrase string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Add test
switch t := value.(type) {
case []byte:
return DumpBytesToString(t), nil
case string:
for i, c := range t {
if c == 0 {
t = t[:i]
break
}
}
if isPrintableText(t) == false {
phrase = fmt.Sprintf("string with binary data (%d bytes)", len(t))
return phrase, nil
}
return t, nil
case []uint16, []uint32, []int32, []float64, []float32:
val := reflect.ValueOf(t)
if val.Len() == 0 {
return "", nil
}
if justFirst == true {
var valueSuffix string
if val.Len() > 1 {
valueSuffix = "..."
}
return fmt.Sprintf("%v%s", val.Index(0), valueSuffix), nil
}
return fmt.Sprintf("%v", val), nil
case []Rational:
if len(t) == 0 {
return "", nil
}
parts := make([]string, len(t))
for i, r := range t {
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
if justFirst == true {
break
}
}
if justFirst == true {
var valueSuffix string
if len(t) > 1 {
valueSuffix = "..."
}
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
}
return fmt.Sprintf("%v", parts), nil
case []SignedRational:
if len(t) == 0 {
return "", nil
}
parts := make([]string, len(t))
for i, r := range t {
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
if justFirst == true {
break
}
}
if justFirst == true {
var valueSuffix string
if len(t) > 1 {
valueSuffix = "..."
}
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
}
return fmt.Sprintf("%v", parts), nil
case fmt.Stringer:
s := t.String()
if isPrintableText(s) == false {
phrase = fmt.Sprintf("stringable with binary data (%d bytes)", len(s))
return phrase, nil
}
// An undefined value that is documented (or that we otherwise support).
return s, nil
default:
// Affects only "unknown" values, in general.
log.Panicf("type can not be formatted into string: %v", reflect.TypeOf(value).Name())
// Never called.
return "", nil
}
}
// Format returns a stringified value for the given encoding. Automatically
// parses. Automatically calculates count based on type size.
func FormatFromBytes(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (phrase string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Add test
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[tagType], typeSize)
}
// unitCount is the calculated unit-count. This should equal the original
// value from the tag (pre-resolution).
unitCount := uint32(len(rawBytes) / typeSize)
// Truncate the items if it's not bytes or a string and we just want the first.
var value interface{}
switch tagType {
case TypeByte:
var err error
value, err = parser.ParseBytes(rawBytes, unitCount)
log.PanicIf(err)
case TypeAscii:
var err error
value, err = parser.ParseAscii(rawBytes, unitCount)
log.PanicIf(err)
case TypeAsciiNoNul:
var err error
value, err = parser.ParseAsciiNoNul(rawBytes, unitCount)
log.PanicIf(err)
case TypeShort:
var err error
value, err = parser.ParseShorts(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
case TypeLong:
var err error
value, err = parser.ParseLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
case TypeFloat:
var err error
value, err = parser.ParseFloats(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
case TypeDouble:
var err error
value, err = parser.ParseDoubles(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
case TypeRational:
var err error
value, err = parser.ParseRationals(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
case TypeSignedLong:
var err error
value, err = parser.ParseSignedLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
case TypeSignedRational:
var err error
value, err = parser.ParseSignedRationals(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
default:
// Affects only "unknown" values, in general.
log.Panicf("value of type [%s] can not be formatted into string", tagType.String())
// Never called.
return "", nil
}
phrase, err = FormatFromType(value, justFirst)
log.PanicIf(err)
return phrase, nil
}
// TranslateStringToType converts user-provided strings to properly-typed
// values. If a string, returns a string. Else, assumes that it's a single
// number. If a list needs to be processed, it is the caller's responsibility to
// split it (according to whichever convention has been established).
func TranslateStringToType(tagType TagTypePrimitive, valueString string) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if tagType == TypeUndefined {
// The caller should just call String() on the decoded type.
log.Panicf("undefined-type values are not supported")
}
if tagType == TypeByte {
wide, err := strconv.ParseInt(valueString, 16, 8)
log.PanicIf(err)
return byte(wide), nil
} 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 tagType == TypeShort {
n, err := strconv.ParseUint(valueString, 10, 16)
log.PanicIf(err)
return uint16(n), nil
} else if tagType == TypeLong {
n, err := strconv.ParseUint(valueString, 10, 32)
log.PanicIf(err)
return uint32(n), nil
} else if tagType == TypeRational {
parts := strings.SplitN(valueString, "/", 2)
numerator, err := strconv.ParseUint(parts[0], 10, 32)
log.PanicIf(err)
denominator, err := strconv.ParseUint(parts[1], 10, 32)
log.PanicIf(err)
return Rational{
Numerator: uint32(numerator),
Denominator: uint32(denominator),
}, nil
} else if tagType == TypeSignedLong {
n, err := strconv.ParseInt(valueString, 10, 32)
log.PanicIf(err)
return int32(n), nil
} else if tagType == TypeFloat {
n, err := strconv.ParseFloat(valueString, 32)
log.PanicIf(err)
return float32(n), nil
} else if tagType == TypeDouble {
n, err := strconv.ParseFloat(valueString, 64)
log.PanicIf(err)
return float64(n), nil
} else if tagType == TypeSignedRational {
parts := strings.SplitN(valueString, "/", 2)
numerator, err := strconv.ParseInt(parts[0], 10, 32)
log.PanicIf(err)
denominator, err := strconv.ParseInt(parts[1], 10, 32)
log.PanicIf(err)
return SignedRational{
Numerator: int32(numerator),
Denominator: int32(denominator),
}, nil
}
log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String())
return nil, nil
}
// GetTypeByName returns the `TagTypePrimitive` for the given type name.
// Returns (0) if not valid.
func GetTypeByName(typeName string) (tagType TagTypePrimitive, found bool) {
tagType, found = typeNamesR[typeName]
return tagType, found
}
// BasicTag describes a single tag for any purpose.
type BasicTag struct {
// FqIfdPath is the fully-qualified IFD-path.
FqIfdPath string
// IfdPath is the unindexed IFD-path.
IfdPath string
// TagId is the tag-ID.
TagId uint16
}
func init() {
for typeId, typeName := range TypeNames {
typeNamesR[typeName] = typeId
}
}
+437
View File
@@ -0,0 +1,437 @@
package exifcommon
import (
"math"
"testing"
"github.com/dsoprea/go-logging"
)
func TestTypeByte_String(t *testing.T) {
if TypeByte.String() != "BYTE" {
t.Fatalf("Type name not correct (byte): [%s]", TypeByte.String())
}
}
func TestTypeAscii_String(t *testing.T) {
if TypeAscii.String() != "ASCII" {
t.Fatalf("Type name not correct (ASCII): [%s]", TypeAscii.String())
}
}
func TestTypeAsciiNoNul_String(t *testing.T) {
if TypeAsciiNoNul.String() != "_ASCII_NO_NUL" {
t.Fatalf("Type name not correct (ASCII no-NUL): [%s]", TypeAsciiNoNul.String())
}
}
func TestTypeShort_String(t *testing.T) {
if TypeShort.String() != "SHORT" {
t.Fatalf("Type name not correct (short): [%s]", TypeShort.String())
}
}
func TestTypeLong_String(t *testing.T) {
if TypeLong.String() != "LONG" {
t.Fatalf("Type name not correct (long): [%s]", TypeLong.String())
}
}
func TestTypeRational_String(t *testing.T) {
if TypeRational.String() != "RATIONAL" {
t.Fatalf("Type name not correct (rational): [%s]", TypeRational.String())
}
}
func TestTypeSignedLong_String(t *testing.T) {
if TypeSignedLong.String() != "SLONG" {
t.Fatalf("Type name not correct (signed long): [%s]", TypeSignedLong.String())
}
}
func TestTypeSignedRational_String(t *testing.T) {
if TypeSignedRational.String() != "SRATIONAL" {
t.Fatalf("Type name not correct (signed rational): [%s]", TypeSignedRational.String())
}
}
func TestTypeFloat_String(t *testing.T) {
if TypeFloat.String() != "FLOAT" {
t.Fatalf("Type name not correct (float): [%s]", TypeFloat.String())
}
}
func TestTypeDouble_String(t *testing.T) {
if TypeDouble.String() != "DOUBLE" {
t.Fatalf("Type name not correct (double): [%s]", TypeDouble.String())
}
}
func TestTypeByte_Size(t *testing.T) {
if TypeByte.Size() != 1 {
t.Fatalf("Type size not correct (byte): (%d)", TypeByte.Size())
}
}
func TestTypeAscii_Size(t *testing.T) {
if TypeAscii.Size() != 1 {
t.Fatalf("Type size not correct (ASCII): (%d)", TypeAscii.Size())
}
}
func TestTypeAsciiNoNul_Size(t *testing.T) {
if TypeAsciiNoNul.Size() != 1 {
t.Fatalf("Type size not correct (ASCII no-NUL): (%d)", TypeAsciiNoNul.Size())
}
}
func TestTypeShort_Size(t *testing.T) {
if TypeShort.Size() != 2 {
t.Fatalf("Type size not correct (short): (%d)", TypeShort.Size())
}
}
func TestTypeLong_Size(t *testing.T) {
if TypeLong.Size() != 4 {
t.Fatalf("Type size not correct (long): (%d)", TypeLong.Size())
}
}
func TestTypeRational_Size(t *testing.T) {
if TypeRational.Size() != 8 {
t.Fatalf("Type size not correct (rational): (%d)", TypeRational.Size())
}
}
func TestTypeSignedLong_Size(t *testing.T) {
if TypeSignedLong.Size() != 4 {
t.Fatalf("Type size not correct (signed long): (%d)", TypeSignedLong.Size())
}
}
func TestTypeSignedRational_Size(t *testing.T) {
if TypeSignedRational.Size() != 8 {
t.Fatalf("Type size not correct (signed rational): (%d)", TypeSignedRational.Size())
}
}
func TestTypeFloat_Size(t *testing.T) {
if TypeFloat.Size() != 4 {
t.Fatalf("Type size not correct (float): (%d)", TypeFloat.Size())
}
}
func TestTypeDouble_Size(t *testing.T) {
if TypeDouble.Size() != 8 {
t.Fatalf("Type size not correct (double): (%d)", TypeDouble.Size())
}
}
func TestFormat__Byte(t *testing.T) {
r := []byte{1, 2, 3, 4, 5, 6, 7, 8}
s, err := FormatFromBytes(r, TypeByte, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "01 02 03 04 05 06 07 08" {
t.Fatalf("Format output not correct (bytes): [%s]", s)
}
}
func TestFormat__Ascii(t *testing.T) {
r := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
s, err := FormatFromBytes(r, TypeAscii, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "abcdefg" {
t.Fatalf("Format output not correct (ASCII): [%s]", s)
}
}
func TestFormat__AsciiNoNul(t *testing.T) {
r := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
s, err := FormatFromBytes(r, TypeAsciiNoNul, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "abcdefgh" {
t.Fatalf("Format output not correct (ASCII no-NUL): [%s]", s)
}
}
func TestFormat__Short(t *testing.T) {
r := []byte{0, 1, 0, 2}
s, err := FormatFromBytes(r, TypeShort, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[1 2]" {
t.Fatalf("Format output not correct (shorts): [%s]", s)
}
}
func TestFormat__Long(t *testing.T) {
r := []byte{0, 0, 0, 1, 0, 0, 0, 2}
s, err := FormatFromBytes(r, TypeLong, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[1 2]" {
t.Fatalf("Format output not correct (longs): [%s]", s)
}
}
func TestFormat__Float(t *testing.T) {
r := []byte{0x3f, 0x80, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00}
s, err := FormatFromBytes(r, TypeFloat, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[1 2]" {
t.Fatalf("Format output not correct (floats): [%s]", s)
}
}
func TestFormat__Double(t *testing.T) {
r := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1,
0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd}
s, err := FormatFromBytes(r, TypeDouble, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[3.14159265 2.71828182]" {
t.Fatalf("Format output not correct (doubles): [%s]", s)
}
}
func TestFormat__Rational(t *testing.T) {
r := []byte{
0, 0, 0, 1, 0, 0, 0, 2,
0, 0, 0, 3, 0, 0, 0, 4,
}
s, err := FormatFromBytes(r, TypeRational, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[1/2 3/4]" {
t.Fatalf("Format output not correct (rationals): [%s]", s)
}
}
func TestFormat__SignedLong(t *testing.T) {
r := []byte{0, 0, 0, 1, 0, 0, 0, 2}
s, err := FormatFromBytes(r, TypeSignedLong, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[1 2]" {
t.Fatalf("Format output not correct (signed longs): [%s]", s)
}
}
func TestFormat__SignedRational(t *testing.T) {
r := []byte{
0, 0, 0, 1, 0, 0, 0, 2,
0, 0, 0, 3, 0, 0, 0, 4,
}
s, err := FormatFromBytes(r, TypeSignedRational, false, TestDefaultByteOrder)
log.PanicIf(err)
if s != "[1/2 3/4]" {
t.Fatalf("Format output not correct (signed rationals): [%s]", s)
}
}
func TestFormat__Undefined(t *testing.T) {
r := []byte{'a', 'b'}
_, err := FormatFromBytes(r, TypeUndefined, false, TestDefaultByteOrder)
if err == nil {
t.Fatalf("Expected error.")
} else if err.Error() != "can not determine tag-value size for type (7): [UNDEFINED]" {
log.Panic(err)
}
}
func TestTranslateStringToType__TypeUndefined(t *testing.T) {
_, err := TranslateStringToType(TypeUndefined, "")
if err == nil {
t.Fatalf("Expected error.")
} else if err.Error() != "undefined-type values are not supported" {
log.Panic(err)
}
}
func TestTranslateStringToType__TypeByte(t *testing.T) {
v, err := TranslateStringToType(TypeByte, "02")
log.PanicIf(err)
if v != byte(2) {
t.Fatalf("Translation of string to type not correct (bytes): %v", v)
}
}
func TestTranslateStringToType__TypeAscii(t *testing.T) {
v, err := TranslateStringToType(TypeAscii, "abcdefgh")
log.PanicIf(err)
if v != "abcdefgh" {
t.Fatalf("Translation of string to type not correct (ascii): %v", v)
}
}
func TestTranslateStringToType__TypeAsciiNoNul(t *testing.T) {
v, err := TranslateStringToType(TypeAsciiNoNul, "abcdefgh")
log.PanicIf(err)
if v != "abcdefgh" {
t.Fatalf("Translation of string to type not correct (ascii no-NUL): %v", v)
}
}
func TestTranslateStringToType__TypeShort(t *testing.T) {
v, err := TranslateStringToType(TypeShort, "11")
log.PanicIf(err)
if v != uint16(11) {
t.Fatalf("Translation of string to type not correct (short): %v", v)
}
}
func TestTranslateStringToType__TypeLong(t *testing.T) {
v, err := TranslateStringToType(TypeLong, "11")
log.PanicIf(err)
if v != uint32(11) {
t.Fatalf("Translation of string to type not correct (long): %v", v)
}
}
func TestTranslateStringToType__TypeFloat(t *testing.T) {
v, err := TranslateStringToType(TypeFloat, "3.14159265")
log.PanicIf(err)
expected := float32(3.14159265)
if v.(float32) < expected || v.(float32) >= math.Nextafter32(expected, expected+1) {
t.Fatalf("Translation of string to type not correct (float32): %v", v)
}
}
func TestTranslateStringToType__TypeDouble(t *testing.T) {
v, err := TranslateStringToType(TypeDouble, "3.14159265")
log.PanicIf(err)
expected := float64(3.14159265)
if v.(float64) < expected || v.(float64) >= math.Nextafter(expected, expected+1) {
t.Fatalf("Translation of string to type not correct (double): %v", v)
}
}
func TestTranslateStringToType__TypeRational(t *testing.T) {
v, err := TranslateStringToType(TypeRational, "11/22")
log.PanicIf(err)
r := v.(Rational)
if r.Numerator != 11 || r.Denominator != 22 {
t.Fatalf("Translation of string to type not correct (rational): %v", r)
}
}
func TestTranslateStringToType__TypeSignedLong(t *testing.T) {
v, err := TranslateStringToType(TypeSignedLong, "11")
log.PanicIf(err)
if v != int32(11) {
t.Fatalf("Translation of string to type not correct (signed long): %v", v)
}
}
func TestTranslateStringToType__TypeSignedRational(t *testing.T) {
v, err := TranslateStringToType(TypeSignedRational, "11/22")
log.PanicIf(err)
r := v.(SignedRational)
if r.Numerator != 11 || r.Denominator != 22 {
t.Fatalf("Translation of string to type not correct (signed rational): %v", r)
}
}
func TestTranslateStringToType__InvalidType(t *testing.T) {
_, err := TranslateStringToType(99, "11/22")
if err == nil {
t.Fatalf("Expected error for invalid type.")
} else if err.Error() != "from-string encoding for type not supported; this shouldn't happen: []" {
log.Panic(err)
}
}
// } else if tagType == TypeLong {
// n, err := strconv.ParseUint(valueString, 10, 32)
// log.PanicIf(err)
// return uint32(n), nil
// } else if tagType == TypeRational {
// parts := strings.SplitN(valueString, "/", 2)
// numerator, err := strconv.ParseUint(parts[0], 10, 32)
// log.PanicIf(err)
// denominator, err := strconv.ParseUint(parts[1], 10, 32)
// log.PanicIf(err)
// return Rational{
// Numerator: uint32(numerator),
// Denominator: uint32(denominator),
// }, nil
// } else if tagType == TypeSignedLong {
// n, err := strconv.ParseInt(valueString, 10, 32)
// log.PanicIf(err)
// return int32(n), nil
// } else if tagType == TypeSignedRational {
// parts := strings.SplitN(valueString, "/", 2)
// numerator, err := strconv.ParseInt(parts[0], 10, 32)
// log.PanicIf(err)
// denominator, err := strconv.ParseInt(parts[1], 10, 32)
// log.PanicIf(err)
// return SignedRational{
// Numerator: int32(numerator),
// Denominator: int32(denominator),
// }, nil
// }
// log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String())
// return nil, nil
// }
func TestIsPrintableText_letters(t *testing.T) {
if isPrintableText("abc") != true {
t.Fatalf("Printable text interpreted as nonprintable.")
}
}
func TestIsPrintableText_space(t *testing.T) {
if isPrintableText(" ") != true {
t.Fatalf("Printable text interpreted as nonprintable.")
}
}
func TestIsPrintableText_newlines(t *testing.T) {
if isPrintableText("\r\n") != true {
t.Fatalf("Printable text interpreted as nonprintable.")
}
}
func TestIsPrintableText_punctuationAndSymbols(t *testing.T) {
if isPrintableText(",:-/$©") != true {
t.Fatalf("Printable text interpreted as nonprintable.")
}
}
+148
View File
@@ -0,0 +1,148 @@
package exifcommon
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/dsoprea/go-logging"
)
var (
timeType = reflect.TypeOf(time.Time{})
)
// DumpBytes prints a list of hex-encoded bytes.
func DumpBytes(data []byte) {
fmt.Printf("DUMP: ")
for _, x := range data {
fmt.Printf("%02x ", x)
}
fmt.Printf("\n")
}
// DumpBytesClause prints a list like DumpBytes(), but encapsulated in
// "[]byte { ... }".
func DumpBytesClause(data []byte) {
fmt.Printf("DUMP: ")
fmt.Printf("[]byte { ")
for i, x := range data {
fmt.Printf("0x%02x", x)
if i < len(data)-1 {
fmt.Printf(", ")
}
}
fmt.Printf(" }\n")
}
// DumpBytesToString returns a stringified list of hex-encoded bytes.
func DumpBytesToString(data []byte) string {
b := new(bytes.Buffer)
for i, x := range data {
_, err := b.WriteString(fmt.Sprintf("%02x", x))
log.PanicIf(err)
if i < len(data)-1 {
_, err := b.WriteRune(' ')
log.PanicIf(err)
}
}
return b.String()
}
// DumpBytesClauseToString returns a comma-separated list of hex-encoded bytes.
func DumpBytesClauseToString(data []byte) string {
b := new(bytes.Buffer)
for i, x := range data {
_, err := b.WriteString(fmt.Sprintf("0x%02x", x))
log.PanicIf(err)
if i < len(data)-1 {
_, err := b.WriteString(", ")
log.PanicIf(err)
}
}
return b.String()
}
// ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a
// `time.Time` struct. It will attempt to convert to UTC first.
func ExifFullTimestampString(t time.Time) (fullTimestampPhrase string) {
t = t.UTC()
return fmt.Sprintf("%04d:%02d:%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}
// ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC
// `time.Time` struct.
func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
parts := strings.Split(fullTimestampPhrase, " ")
datestampValue, timestampValue := parts[0], parts[1]
// Normalize the separators.
datestampValue = strings.ReplaceAll(datestampValue, "-", ":")
timestampValue = strings.ReplaceAll(timestampValue, "-", ":")
dateParts := strings.Split(datestampValue, ":")
year, err := strconv.ParseUint(dateParts[0], 10, 16)
if err != nil {
log.Panicf("could not parse year")
}
month, err := strconv.ParseUint(dateParts[1], 10, 8)
if err != nil {
log.Panicf("could not parse month")
}
day, err := strconv.ParseUint(dateParts[2], 10, 8)
if err != nil {
log.Panicf("could not parse day")
}
timeParts := strings.Split(timestampValue, ":")
hour, err := strconv.ParseUint(timeParts[0], 10, 8)
if err != nil {
log.Panicf("could not parse hour")
}
minute, err := strconv.ParseUint(timeParts[1], 10, 8)
if err != nil {
log.Panicf("could not parse minute")
}
second, err := strconv.ParseUint(timeParts[2], 10, 8)
if err != nil {
log.Panicf("could not parse second")
}
timestamp = time.Date(int(year), time.Month(month), int(day), int(hour), int(minute), int(second), 0, time.UTC)
return timestamp, nil
}
// IsTime returns true if the value is a `time.Time`.
func IsTime(v interface{}) bool {
// TODO(dustin): Add test
return reflect.TypeOf(v) == timeType
}
+80
View File
@@ -0,0 +1,80 @@
package exifcommon
import (
"fmt"
"testing"
"time"
"github.com/dsoprea/go-logging"
)
func TestDumpBytes(t *testing.T) {
DumpBytes([]byte{1, 2, 3, 4})
}
func TestDumpBytesClause(t *testing.T) {
DumpBytesClause([]byte{1, 2, 3, 4})
}
func TestDumpBytesToString(t *testing.T) {
s := DumpBytesToString([]byte{1, 2, 3, 4})
if s != "01 02 03 04" {
t.Fatalf("String not correct: [%s]", s)
}
}
func TestDumpBytesClauseToString(t *testing.T) {
s := DumpBytesClauseToString([]byte{1, 2, 3, 4})
if s != "0x01, 0x02, 0x03, 0x04" {
t.Fatalf("Stringified clause is not correct: [%s]", s)
}
}
func TestExifFullTimestampString(t *testing.T) {
originalPhrase := "2018:11:30 13:01:49"
timestamp, err := ParseExifFullTimestamp(originalPhrase)
log.PanicIf(err)
restoredPhrase := ExifFullTimestampString(timestamp)
if restoredPhrase != originalPhrase {
t.Fatalf("Final phrase [%s] does not equal original phrase [%s]", restoredPhrase, originalPhrase)
}
}
func ExampleExifFullTimestampString() {
originalPhrase := "2018:11:30 13:01:49"
timestamp, err := ParseExifFullTimestamp(originalPhrase)
log.PanicIf(err)
restoredPhrase := ExifFullTimestampString(timestamp)
fmt.Printf("To EXIF timestamp: [%s]\n", restoredPhrase)
// Output:
// To EXIF timestamp: [2018:11:30 13:01:49]
}
func TestParseExifFullTimestamp(t *testing.T) {
timestamp, err := ParseExifFullTimestamp("2018:11:30 13:01:49")
log.PanicIf(err)
actual := timestamp.Format(time.RFC3339)
expected := "2018-11-30T13:01:49Z"
if actual != expected {
t.Fatalf("time not formatted correctly: [%s] != [%s]", actual, expected)
}
}
func ExampleParseExifFullTimestamp() {
originalPhrase := "2018:11:30 13:01:49"
timestamp, err := ParseExifFullTimestamp(originalPhrase)
log.PanicIf(err)
fmt.Printf("To Go timestamp: [%s]\n", timestamp.Format(time.RFC3339))
// Output:
// To Go timestamp: [2018-11-30T13:01:49Z]
}
+464
View File
@@ -0,0 +1,464 @@
package exifcommon
import (
"errors"
"io"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
parser *Parser
)
var (
// ErrNotFarValue indicates that an offset-based lookup was attempted for a
// non-offset-based (embedded) value.
ErrNotFarValue = errors.New("not a far value")
)
// ValueContext embeds all of the parameters required to find and extract the
// actual tag value.
type ValueContext struct {
unitCount uint32
valueOffset uint32
rawValueOffset []byte
rs io.ReadSeeker
tagType TagTypePrimitive
byteOrder binary.ByteOrder
// undefinedValueTagType is the effective type to use if this is an
// "undefined" value.
undefinedValueTagType TagTypePrimitive
ifdPath string
tagId uint16
}
// TODO(dustin): We can update newValueContext() to derive `valueOffset` itself (from `rawValueOffset`).
// NewValueContext returns a new ValueContext struct.
func NewValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset []byte, rs io.ReadSeeker, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext {
return &ValueContext{
unitCount: unitCount,
valueOffset: valueOffset,
rawValueOffset: rawValueOffset,
rs: rs,
tagType: tagType,
byteOrder: byteOrder,
ifdPath: ifdPath,
tagId: tagId,
}
}
// SetUndefinedValueType sets the effective type if this is an unknown-type tag.
func (vc *ValueContext) SetUndefinedValueType(tagType TagTypePrimitive) {
if vc.tagType != TypeUndefined {
log.Panicf("can not set effective type for unknown-type tag because this is *not* an unknown-type tag")
}
vc.undefinedValueTagType = tagType
}
// UnitCount returns the embedded unit-count.
func (vc *ValueContext) UnitCount() uint32 {
return vc.unitCount
}
// ValueOffset returns the value-offset decoded as a `uint32`.
func (vc *ValueContext) ValueOffset() uint32 {
return vc.valueOffset
}
// RawValueOffset returns the uninterpreted value-offset. This is used for
// embedded values (values small enough to fit within the offset bytes rather
// than needing to be stored elsewhere and referred to by an actual offset).
func (vc *ValueContext) RawValueOffset() []byte {
return vc.rawValueOffset
}
// AddressableData returns the block of data that we can dereference into.
func (vc *ValueContext) AddressableData() io.ReadSeeker {
// RELEASE)dustin): Rename from AddressableData() to ReadSeeker()
return vc.rs
}
// ByteOrder returns the byte-order of numbers.
func (vc *ValueContext) ByteOrder() binary.ByteOrder {
return vc.byteOrder
}
// IfdPath returns the path of the IFD containing this tag.
func (vc *ValueContext) IfdPath() string {
return vc.ifdPath
}
// TagId returns the ID of the tag that we represent.
func (vc *ValueContext) TagId() uint16 {
return vc.tagId
}
// 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 := vc.effectiveValueType()
return (tagType.Size() * int(vc.unitCount)) <= 4
}
// SizeInBytes returns the number of bytes that this value requires. The
// underlying call will panic if the type is UNDEFINED. It is the
// responsibility of the caller to preemptively check that.
func (vc *ValueContext) SizeInBytes() int {
tagType := vc.effectiveValueType()
return tagType.Size() * int(vc.unitCount)
}
// effectiveValueType returns the effective type of the unknown-type tag or, if
// not unknown, the actual type.
func (vc *ValueContext) effectiveValueType() (tagType TagTypePrimitive) {
if vc.tagType == TypeUndefined {
tagType = vc.undefinedValueTagType
if tagType == 0 {
log.Panicf("undefined-value type not set")
}
} else {
tagType = vc.tagType
}
return tagType
}
// readRawEncoded returns the encoded bytes for the value that we represent.
func (vc *ValueContext) readRawEncoded() (rawBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
tagType := vc.effectiveValueType()
unitSizeRaw := uint32(tagType.Size())
if vc.isEmbedded() == true {
byteLength := unitSizeRaw * vc.unitCount
return vc.rawValueOffset[:byteLength], nil
}
_, err = vc.rs.Seek(int64(vc.valueOffset), io.SeekStart)
log.PanicIf(err)
rawBytes = make([]byte, vc.unitCount*unitSizeRaw)
_, err = io.ReadFull(vc.rs, rawBytes)
log.PanicIf(err)
return rawBytes, nil
}
// GetFarOffset returns the offset if the value is not embedded [within the
// pointer itself] or an error if an embedded value.
func (vc *ValueContext) GetFarOffset() (offset uint32, err error) {
if vc.isEmbedded() == true {
return 0, ErrNotFarValue
}
return vc.valueOffset, nil
}
// ReadRawEncoded returns the encoded bytes for the value that we represent.
func (vc *ValueContext) ReadRawEncoded() (rawBytes []byte, err error) {
// TODO(dustin): Remove this method and rename readRawEncoded in its place.
return vc.readRawEncoded()
}
// 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
// `Undefined()`.
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 := FormatFromBytes(rawBytes, vc.effectiveValueType(), false, vc.byteOrder)
log.PanicIf(err)
return phrase, nil
}
// FormatFirst 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 := FormatFromBytes(rawBytes, vc.tagType, true, vc.byteOrder)
log.PanicIf(err)
return phrase, nil
}
// ReadBytes parses the encoded byte-array from the value-context.
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
}
// ReadAscii parses the encoded NUL-terminated ASCII string from the value-
// context.
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
}
// ReadAsciiNoNul parses the non-NUL-terminated encoded ASCII string from the
// value-context.
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
}
// ReadShorts parses the list of encoded shorts from the value-context.
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
}
// ReadLongs parses the list of encoded, unsigned longs from the value-context.
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
}
// ReadFloats parses the list of encoded, floats from the value-context.
func (vc *ValueContext) ReadFloats() (value []float32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseFloats(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
// ReadDoubles parses the list of encoded, doubles from the value-context.
func (vc *ValueContext) ReadDoubles() (value []float64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseDoubles(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
// ReadRationals parses the list of encoded, unsigned rationals from the value-
// context.
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
}
// ReadSignedLongs parses the list of encoded, signed longs from the value-context.
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
}
// ReadSignedRationals parses the list of encoded, signed rationals from the
// value-context.
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
// `Undefined()`.
func (vc *ValueContext) Values() (values interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if vc.tagType == TypeByte {
values, err = vc.ReadBytes()
log.PanicIf(err)
} else if vc.tagType == TypeAscii {
values, err = vc.ReadAscii()
log.PanicIf(err)
} else if vc.tagType == TypeAsciiNoNul {
values, err = vc.ReadAsciiNoNul()
log.PanicIf(err)
} else if vc.tagType == TypeShort {
values, err = vc.ReadShorts()
log.PanicIf(err)
} else if vc.tagType == TypeLong {
values, err = vc.ReadLongs()
log.PanicIf(err)
} else if vc.tagType == TypeRational {
values, err = vc.ReadRationals()
log.PanicIf(err)
} else if vc.tagType == TypeSignedLong {
values, err = vc.ReadSignedLongs()
log.PanicIf(err)
} else if vc.tagType == TypeSignedRational {
values, err = vc.ReadSignedRationals()
log.PanicIf(err)
} else if vc.tagType == TypeFloat {
values, err = vc.ReadFloats()
log.PanicIf(err)
} else if vc.tagType == TypeDouble {
values, err = vc.ReadDoubles()
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 values, nil
}
func init() {
parser = new(Parser)
}
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
package exifcommon
import (
"bytes"
"math"
"reflect"
"time"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
typeEncodeLogger = log.NewLogger("exif.type_encode")
)
// EncodedData encapsulates the compound output of an encoding operation.
type EncodedData struct {
Type TagTypePrimitive
Encoded []byte
// TODO(dustin): Is this really necessary? We might have this just to correlate to the incoming stream format (raw bytes and a unit-count both for incoming and outgoing).
UnitCount uint32
}
// ValueEncoder knows how to encode values of every type to bytes.
type ValueEncoder struct {
byteOrder binary.ByteOrder
}
// NewValueEncoder returns a new ValueEncoder.
func NewValueEncoder(byteOrder binary.ByteOrder) *ValueEncoder {
return &ValueEncoder{
byteOrder: byteOrder,
}
}
func (ve *ValueEncoder) encodeBytes(value []uint8) (ed EncodedData, err error) {
ed.Type = TypeByte
ed.Encoded = []byte(value)
ed.UnitCount = uint32(len(value))
return ed, nil
}
func (ve *ValueEncoder) encodeAscii(value string) (ed EncodedData, err error) {
ed.Type = TypeAscii
ed.Encoded = []byte(value)
ed.Encoded = append(ed.Encoded, 0)
ed.UnitCount = uint32(len(ed.Encoded))
return ed, nil
}
// encodeAsciiNoNul returns a string encoded as a byte-string without a trailing
// NUL byte.
//
// Note that:
//
// 1. This type can not be automatically encoded using `Encode()`. The default
// mode is to encode *with* a trailing NUL byte using `encodeAscii`. Only
// certain undefined-type tags using an unterminated ASCII string and these
// are exceptional in nature.
//
// 2. The presence of this method allows us to completely test the complimentary
// no-nul parser.
//
func (ve *ValueEncoder) encodeAsciiNoNul(value string) (ed EncodedData, err error) {
ed.Type = TypeAsciiNoNul
ed.Encoded = []byte(value)
ed.UnitCount = uint32(len(ed.Encoded))
return ed, nil
}
func (ve *ValueEncoder) encodeShorts(value []uint16) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*2)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint16(ed.Encoded[i*2:(i+1)*2], value[i])
}
ed.Type = TypeShort
return ed, nil
}
func (ve *ValueEncoder) encodeLongs(value []uint32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*4)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*4:(i+1)*4], value[i])
}
ed.Type = TypeLong
return ed, nil
}
func (ve *ValueEncoder) encodeFloats(value []float32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*4)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*4:(i+1)*4], math.Float32bits(value[i]))
}
ed.Type = TypeFloat
return ed, nil
}
func (ve *ValueEncoder) encodeDoubles(value []float64) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*8)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint64(ed.Encoded[i*8:(i+1)*8], math.Float64bits(value[i]))
}
ed.Type = TypeDouble
return ed, nil
}
func (ve *ValueEncoder) encodeRationals(value []Rational) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*8)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*8+0:i*8+4], value[i].Numerator)
ve.byteOrder.PutUint32(ed.Encoded[i*8+4:i*8+8], value[i].Denominator)
}
ed.Type = TypeRational
return ed, nil
}
func (ve *ValueEncoder) encodeSignedLongs(value []int32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
for i := uint32(0); i < ed.UnitCount; i++ {
err := binary.Write(b, ve.byteOrder, value[i])
log.PanicIf(err)
}
ed.Type = TypeSignedLong
ed.Encoded = b.Bytes()
return ed, nil
}
func (ve *ValueEncoder) encodeSignedRationals(value []SignedRational) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
for i := uint32(0); i < ed.UnitCount; i++ {
err := binary.Write(b, ve.byteOrder, value[i].Numerator)
log.PanicIf(err)
err = binary.Write(b, ve.byteOrder, value[i].Denominator)
log.PanicIf(err)
}
ed.Type = TypeSignedRational
ed.Encoded = b.Bytes()
return ed, nil
}
// Encode returns bytes for the given value, infering type from the actual
// value. This does not support `TypeAsciiNoNull` (all strings are encoded as
// `TypeAscii`).
func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
switch t := value.(type) {
case []byte:
ed, err = ve.encodeBytes(t)
log.PanicIf(err)
case string:
ed, err = ve.encodeAscii(t)
log.PanicIf(err)
case []uint16:
ed, err = ve.encodeShorts(t)
log.PanicIf(err)
case []uint32:
ed, err = ve.encodeLongs(t)
log.PanicIf(err)
case []float32:
ed, err = ve.encodeFloats(t)
log.PanicIf(err)
case []float64:
ed, err = ve.encodeDoubles(t)
log.PanicIf(err)
case []Rational:
ed, err = ve.encodeRationals(t)
log.PanicIf(err)
case []int32:
ed, err = ve.encodeSignedLongs(t)
log.PanicIf(err)
case []SignedRational:
ed, err = ve.encodeSignedRationals(t)
log.PanicIf(err)
case time.Time:
// For convenience, if the user doesn't want to deal with translation
// semantics with timestamps.
s := ExifFullTimestampString(t)
ed, err = ve.encodeAscii(s)
log.PanicIf(err)
default:
log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
}
return ed, nil
}
+725
View File
@@ -0,0 +1,725 @@
package exifcommon
import (
"bytes"
"math"
"reflect"
"testing"
"time"
"github.com/dsoprea/go-logging"
)
func TestValueEncoder_encodeBytes__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []byte("original text")
ed, err := ve.encodeBytes(original)
log.PanicIf(err)
if ed.Type != TypeByte {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 13 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseBytes(ed.Encoded, ed.UnitCount)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeAscii__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := "original text"
ed, err := ve.encodeAscii(original)
log.PanicIf(err)
if ed.Type != TypeAscii {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
expected = append(expected, 0)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 14 {
t.Fatalf("Unit-count not correct.")
}
// Check that the string was recovered correctly and with the trailing NUL
// character autostripped.
recovered, err := parser.ParseAscii(ed.Encoded, ed.UnitCount)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeAsciiNoNul__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := "original text"
ed, err := ve.encodeAsciiNoNul(original)
log.PanicIf(err)
if ed.Type != TypeAsciiNoNul {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 13 {
t.Fatalf("Unit-count not correct.")
}
// Check that the string was recovered correctly and with the trailing NUL
// character ignored (because not expected in the context of that type).
recovered, err := parser.ParseAsciiNoNul(ed.Encoded, ed.UnitCount)
log.PanicIf(err)
if reflect.DeepEqual(recovered, string(expected)) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeShorts__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint16{0x11, 0x22, 0x33, 0x44, 0x55}
ed, err := ve.encodeShorts(original)
log.PanicIf(err)
if ed.Type != TypeShort {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x11,
0x00, 0x22,
0x00, 0x33,
0x00, 0x44,
0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseShorts(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeLongs__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint32{0x11, 0x22, 0x33, 0x44, 0x55}
ed, err := ve.encodeLongs(original)
log.PanicIf(err)
if ed.Type != TypeLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseLongs(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeFloats__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []float32{3.14159265, 2.71828182, 51.0, 68.0, 85.0}
ed, err := ve.encodeFloats(original)
log.PanicIf(err)
if ed.Type != TypeFloat {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x40, 0x49, 0x0f, 0xdb,
0x40, 0x2d, 0xf8, 0x54,
0x42, 0x4c, 0x00, 0x00,
0x42, 0x88, 0x00, 0x00,
0x42, 0xaa, 0x00, 0x00,
}
if bytes.Equal(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseFloats(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
for i, v := range recovered {
if v < original[i] || v >= math.Nextafter32(original[i], original[i]+1) {
t.Fatalf("ReadFloats expecting %v, received %v", original[i], v)
}
}
}
func TestValueEncoder_encodeDoubles__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []float64{3.14159265, 2.71828182, 954877.1230695, 68.0, 85.0}
ed, err := ve.encodeDoubles(original)
log.PanicIf(err)
if ed.Type != TypeDouble {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1,
0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd,
0x41, 0x2d, 0x23, 0xfa, 0x3f, 0x02, 0xf7, 0x2b,
0x40, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x55, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseDoubles(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
for i, v := range recovered {
if v < original[i] || v >= math.Nextafter(original[i], original[i]+1) {
t.Fatalf("ReadDoubles expecting %v, received %v", original[i], v)
}
}
}
func TestValueEncoder_encodeRationals__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []Rational{
{
Numerator: 0x11,
Denominator: 0x22,
},
{
Numerator: 0x33,
Denominator: 0x44,
},
{
Numerator: 0x55,
Denominator: 0x66,
},
{
Numerator: 0x77,
Denominator: 0x88,
},
{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.encodeRationals(original)
log.PanicIf(err)
if ed.Type != TypeRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseRationals(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeSignedLongs__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []int32{0x11, 0x22, 0x33, 0x44, 0x55}
ed, err := ve.encodeSignedLongs(original)
log.PanicIf(err)
if ed.Type != TypeSignedLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseSignedLongs(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_encodeSignedRationals__Cycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []SignedRational{
{
Numerator: 0x11,
Denominator: 0x22,
},
{
Numerator: 0x33,
Denominator: 0x44,
},
{
Numerator: 0x55,
Denominator: 0x66,
},
{
Numerator: 0x77,
Denominator: 0x88,
},
{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.encodeSignedRationals(original)
log.PanicIf(err)
if ed.Type != TypeSignedRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
recovered, err := parser.ParseSignedRationals(ed.Encoded, ed.UnitCount, byteOrder)
log.PanicIf(err)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestValueEncoder_Encode__Byte(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []byte("original text")
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeByte {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 13 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Ascii(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := "original text"
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeAscii {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
expected = append(expected, 0)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 14 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Short(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint16{0x11, 0x22, 0x33, 0x44, 0x55}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeShort {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x11,
0x00, 0x22,
0x00, 0x33,
0x00, 0x44,
0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Long(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint32{0x11, 0x22, 0x33, 0x44, 0x55}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Float(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []float32{3.14159265, 2.71828182, 51.0, 68.0, 85.0}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeFloat {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x40, 0x49, 0x0f, 0xdb,
0x40, 0x2d, 0xf8, 0x54,
0x42, 0x4c, 0x00, 0x00,
0x42, 0x88, 0x00, 0x00,
0x42, 0xaa, 0x00, 0x00,
}
if bytes.Equal(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Double(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []float64{3.14159265, 2.71828182, 954877.1230695, 68.0, 85.0}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeDouble {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1,
0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd,
0x41, 0x2d, 0x23, 0xfa, 0x3f, 0x02, 0xf7, 0x2b,
0x40, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x55, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
}
if bytes.Equal(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Rational(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []Rational{
{
Numerator: 0x11,
Denominator: 0x22,
},
{
Numerator: 0x33,
Denominator: 0x44,
},
{
Numerator: 0x55,
Denominator: 0x66,
},
{
Numerator: 0x77,
Denominator: 0x88,
},
{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__SignedLong(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []int32{0x11, 0x22, 0x33, 0x44, 0x55}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeSignedLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__SignedRational(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []SignedRational{
{
Numerator: 0x11,
Denominator: 0x22,
},
{
Numerator: 0x33,
Denominator: 0x44,
},
{
Numerator: 0x55,
Denominator: 0x66,
},
{
Numerator: 0x77,
Denominator: 0x88,
},
{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeSignedRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte{
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestValueEncoder_Encode__Timestamp(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
now := time.Now()
ed, err := ve.Encode(now)
log.PanicIf(err)
if ed.Type != TypeAscii {
t.Fatalf("Timestamp not encoded as ASCII.")
}
expectedTimestampBytes := ExifFullTimestampString(now)
// Leave an extra byte for the NUL.
expected := make([]byte, len(expectedTimestampBytes)+1)
copy(expected, expectedTimestampBytes)
if bytes.Equal(ed.Encoded, expected) != true {
t.Fatalf("Timestamp not encoded correctly: [%s] != [%s]", string(ed.Encoded), string(expected))
}
}