Skip to content

Releases: FlavioCFOliveira/GoMetadata

v1.2.0

10 Jun 08:33

Choose a tag to compare

[1.2.0] - 2026-06-10

Added

  • Write support for all 13 container formats (write.go, format/format.go): format.SupportsWrite now returns true for every supported format. The only remaining write limitation is BigTIFF: Write and WriteFile return ErrWriteNotSupported when the source is a BigTIFF file (magic 0x002B).

  • TIFF copy-and-relocate metadata write (format/tiff/, #92/#93): tiff.Inject uses a copy-and-relocate serialiser (format/tiff/relocate.go) that enumerates every image-data block referenced by StripOffsets (0x0111), TileOffsets (0x0144), and JPEGInterchangeFormat (0x0201 non-thumbnail) across IFD0 and the IFD1 chain, appends each block at a fresh absolute offset in the rebuilt TIFF stream, and patches the offset entries accordingly. Both scalar (single-strip) and array (multi-strip, multi-tile, COUNT > 1) offset entries are handled. New sentinel errors tiff.ErrBlockOutOfBounds, tiff.ErrUnsupportedOffsetType, tiff.ErrTruncatedOffsetArray, and tiff.ErrUnsupportedElemSize are exported for diagnostic use.

  • DNG SubIFD recursive relocation and write support (format/tiff/relocate.go, #94 + #98): the copy-and-relocate serialiser recursively follows SubIFDs (tag 0x014A) from IFD0, enumerates their strip/tile image blocks, and relocates both the SubIFD structures and their image blocks. The fix for #98 ensures that all out-of-line value areas (RATIONAL, SRATIONAL, DOUBLE, long ASCII, etc.) within each SubIFD have their valOrOff pointers updated to the new absolute positions, preventing XResolution/YResolution and similar fields from becoming undefined after write. Validated against a real Pentax QS1 DNG corpus file: ImageDataHash IN==OUT. Sentinel errors tiff.ErrSubIFDPointerArrayOOB and tiff.ErrSubIFDEntryNotFound are exported for diagnostic use.

  • CR2 metadata write via copy-and-relocate (#95): Canon CR2 uses standard LE TIFF magic (II*\0) and routes through the same writeTIFF copy-and-relocate path as TIFF and DNG. Canon MakerNote blobs are copied verbatim (blob-relative offsets; move-safe). Validated against a real Canon EOS 350D corpus file: ImageDataHash IN==OUT, all MakerNote and SubIFD tags preserved.

  • NEF metadata write (#102): the NEF-specific write path extends the Nikon Type-3 MakerNote blob to cover PreviewIFD and NikonScanIFD (which live beyond the declared byte count in the outer TIFF entry), enumerates the PreviewIFD image block, and patches the MakerNote-relative 0x0201 offset after re-encoding. Validated against a real Nikon D70 NEF corpus file: ImageDataHash IN==OUT, all metadata preserved.

  • ARW metadata write (#103): the ARW-specific write path rebases all Sony MakerNote out-of-line offsets (Sony uses TIFF-absolute offsets, not blob-relative), extracts the SR2Private (0xC634) block verbatim, appends it to the output, and patches both the SR2 internal pointers and the IFD0 tag to the new position. Validated against a real Sony DSLR-A500 ARW corpus file: ImageDataHash IN==OUT, all 52 MakerNote tags and SR2Private preserved.

  • ORF metadata write (#104): the ORF-specific write path patches the non-standard IIRO/IIRS magic bytes to standard LE TIFF before the copy-and-relocate pass and restores the original magic in the output. Both IIRO (Olympus DSLRs) and IIRS (older compacts) are supported. Validated against real corpus files (Olympus E-M10 and C5050Z).

  • RW2 metadata write (#104): the RW2-specific write path preserves the Panasonic 16-byte device GUID header (bytes [8:24]) and rebases all absolute IFD0 offsets by +16 after GUID insertion. Validated against a real Panasonic DMC-GF1 RW2 corpus file: ImageDataHash IN==OUT.

  • CR3 metadata write with stco/co64 offset relocation (format/raw/cr3/, #91): cr3.Inject rebuilds the Canon UUID box with the new CMTx payloads, then walks every trak → mdia → minf → stbl → {stco, co64} table inside the rebuilt moov, adding delta to each chunk offset pointing at or beyond the original moov end. stco overflow (relocated value > MaxUint32) returns cr3.ErrStcoOverflow.

  • BigTIFF read support (format/tiff/, exif/, #54): the TIFF package now recognises BigTIFF magic (0x002B), reads the 16-byte BigTIFF header (version, offset size, constant), and traverses IFDs using 8-byte offsets and 8-byte value areas. A parameterised IFD traversal in the EXIF package handles both 32-bit (TIFF 6.0) and 64-bit (BigTIFF) IFD layouts. BigTIFF is supported for read only; Write and WriteFile return ErrWriteNotSupported for BigTIFF sources (exif.ErrBigTIFFEncodeNotSupported).

  • Conformance test batteries (docs/conformance/, sprints CF-1 to CF-4, #152–#168): 17 exhaustive conformance test suites covering all supported formats and metadata standards. Each suite maps directly to a normative specification clause (e.g. S-08, IIM-BIN-05, JPEG-04, ROB-03), so a failing test points directly at the violated specification requirement. Contracts are documented in docs/conformance/ (one file per spec family: exif-tiff.md, iptc.md, xmp.md, containers.md).

  • cr3.ErrNoCMT1Box: new exported sentinel returned by cr3.Extract when the Canon UUID structure is present but contains no CMT1 sub-box. The top-level Read converts this to a non-fatal parse warning so that XMP and other metadata remain accessible.

  • cr3.ErrFileTooLarge: new exported sentinel returned when the CR3 input exceeds the 256 MiB read cap.

  • Embedded CI test fixtures (testdata/fixtures/, #195): a curated set of real camera images is now embedded directly in the module, enabling the full test suite to run without downloading a separate corpus. Coverage is maintained at 82.7% with embedded fixtures alone.

  • docs/TESTING.md: new document describing the testing policy, corpus structure, fuzz target inventory, and coverage requirements.

Changed

  • format.SupportsWrite for TIFF, DNG, CR2, NEF, ARW, ORF, RW2, CR3: all now return true. Each format has a dedicated write path with real-corpus validation. The only remaining write limitation is BigTIFF (returns ErrWriteNotSupported).
  • BigTIFF write: Write and WriteFile return ErrWriteNotSupported when the source is a BigTIFF file (magic 0x002B). Use format.SupportsWrite(id) to check write capability programmatically.
  • Write cross-format mismatch detection (#108): Write now returns ErrFormatMismatch when the *Metadata was read from a different container format than the write target, preventing silent data loss from mismatched roundtrips.
  • Write idempotency (#109): calling Write on an already-written byte slice now produces identical output. The fix eliminates a class of bugs where repeated writes caused progressive format divergence.
  • WriteFile atomicity and safety (#124/#125): WriteFile now fsyncs the temporary file before rename, preserves the original file's ownership (uid/gid on Unix), and follows symlinks — writing through to the symlink target rather than replacing the link.
  • MakerNote out-of-line offset rebasing on write (#127): the TIFF write path now rebases all Olympus, Panasonic, Sony, and Nikon Type-3 MakerNote out-of-line offsets when the MakerNote blob is relocated. Previously, relocated MakerNotes had stale absolute file offsets, corrupting all MakerNote values after write.
  • IPTC dataset ascending-order enforcement (#146/#179): iptc.Encode now emits datasets in ascending record-and-dataset-number order as required by IIM §7. Duplicate Dataset 1:00 EnvelopeRecordVersion headers are suppressed when the envelope record is written alongside application-record datasets.
  • XMP GPS decoding via W3C Geo namespace (#195): xmp.GPS() now recognises the W3C Geo namespace (http://www.w3.org/2003/01/geo/wgs84_pos#) in addition to the standard EXIF GPS namespace, allowing GPS coordinates stored by W3C-Geo-aware XMP producers to be decoded correctly.
  • PreserveUnknownSegments option honoured (#85): the PreserveUnknownSegments(true) option is now applied consistently across all format writers. Previously, the option was parsed but silently ignored in several code paths.

Fixed

  • HEIF panic on malformed infe/meta boxes (#106/#169/#177): parseInfeV0V1 now bounds-checks the item protection index read before advancing the position pointer. buildInjectComponents validates metaContentOff <= metaAbsEnd before slicing, closing the panic: slice bounds out of range paths that were confirmed by the fuzzer. AVIF brand detection hardened.
  • HEIF iloc construction method misresolution (#133/#137): parseIlocItemSimple now reads and checks construction_method; items with construction_method != 0 (idat-relative or item-relative extents) are skipped with a diagnostic rather than silently returning garbage bytes from wrong file offsets (ISO 14496-12 §8.11.3).
  • BigTIFF offset truncation (#141/#142/#143): thumbnail offsets, MakerNote offsets, and sub-IFD offsets in BigTIFF files are now read as 64-bit values. Previously, these were read as 32-bit values, causing corrupt seeks and wrong data for BigTIFF files where offsets exceed 4 GiB.
  • EXIF partial-IFD recovery (#126): the IFD parser now recovers usable entries from a truncated IFD rather than discarding all entries when the declared entry count exceeds the available buffer. This brings the parser into conformance with CIPA DC-008 robustness clause R-05.
  • EXIF duplicate tag deduplication (#129): when an IFD contains duplicate tag numbers (malformed file), only the first occurrence is retained. Previously, duplicate tags could cause incorrect value reads depending on which occurrence was returned.
  • EXIF value-overlap detection (#131/#132): the IFD parser now warns (via metaerr) when two entries declare overlapping value regions, and rejects NextIFD pointers that refer to an already-visited offset.
  • **EXIF...
Read more

v1.1.0

03 Jun 08:45

Choose a tag to compare

[1.1.0] - 2026-06-03

Added

  • tiff.ErrUnsupportedMagic: new exported sentinel error returned when the TIFF parser encounters a BigTIFF magic number (0x002B). Previously the library silently misidentified BigTIFF files as ordinary TIFF; now callers can detect and handle this case explicitly with errors.Is(err, tiff.ErrUnsupportedMagic).
  • xmp.ErrDocumentTooLarge: new exported sentinel error returned when an XMP document exceeds the maxXMPDocumentBytes input cap (16 MiB, compile-time constant). Callers can use errors.Is to distinguish this condition from malformed-XML errors.
  • FuzzRead: end-to-end fuzz target at the top-level package (FuzzRead) that drives the full Read orchestrator with arbitrary input. This joins 26 existing fuzz targets for a total of 27.
  • CI fuzz job: a new fuzz CI job runs 6 fuzz targets for 10 seconds each under -race, catching regressions on every pull request.
  • FormatCapability knowledge-graph matrix: the format capability matrix (which combinations of format and operation are supported) is now recorded in the project knowledge graph (the FormatCapability matrix mirroring format.SupportsWrite).

Changed

  • JPEG ExtendedXMP GUID cap: the JPEG parser now caps the number of distinct ExtendedXMP GUIDs at 4 per file during reassembly of multi-segment ExtendedXMP payloads (each GUID is itself capped at 16 MiB, giving a 64 MiB aggregate ceiling). Excess GUIDs beyond the fourth are dropped and the reassembled payload is marked truncated, preventing memory exhaustion from crafted multi-segment JPEGs without aborting the parse.
  • Write-support documentation corrected: README, CHANGELOG, SECURITY.md, and doc.go now precisely state that Write is supported for JPEG, PNG, WebP, HEIF/AVIF, and Canon CR3. TIFF-based containers (TIFF, CR2, NEF, ARW, DNG, ORF, RW2) are read-only; Write returns ErrWriteNotSupported for those formats.
  • go.mod: golang.org/x/text reclassified as a direct dependency (the // indirect annotation removed by go mod tidy).
  • Test coverage: +207 tests added since v1.0.4. New tests cover EXIF/TIFF adversarial fuzz seeds, the internal/iobuf buffer pool (race, contamination, and DoS scenarios), and the top-level read orchestrator.

Fixed

  • iptc.Encode receiver mutation (iptc/iptc.go): Encode previously appended the IPTC 1:90 UTF-8 CodedCharacterSet marker directly to the receiver's Records[0] slice when emitting non-ASCII content, mutating shared state and creating a data race under concurrent Write calls. The fix emits the 1:90 declaration to the encoded output only (via the existing needsUTF8Declaration path); the receiver is now pure and idempotent.
  • internal/iobuf pool hardening (internal/iobuf/iobuf.go): Get(n) now clamps negative n to zero instead of passing it to make, which would have panicked. Put now discards buffers whose capacity exceeds the large-tier canonical cap (largeSize = 65536) rather than returning them, preventing unbounded pool growth from adversarially crafted payloads.
  • DoS caps and write determinism (exif, iptc, xmp, format/jpeg): follow-up fixes from the Sprint 8 re-audit — additional byte-count caps on IFD entry aggregation, IPTC dataset aggregation, and XMP attribute accumulation; deterministic output ordering for EXIF and IPTC write paths.
  • Untrusted-input crash sites (exif, iptc, xmp, format/*): elimination of the remaining nil-dereference and out-of-bounds-slice-index paths reachable from attacker-controlled binary data identified in the Sprint 8 audit.

Security

  • XMP document-level input cap (xmp/): introduced maxXMPDocumentBytes (16 MiB, compile-time constant) as a hard ceiling on the total UTF-8 bytes accepted by a single XMP parse (checked post-normalisation, before the RDF scan). Exceeding the cap returns xmp.ErrDocumentTooLarge without allocating further memory, preventing memory-exhaustion attacks from crafted XMP payloads (CWE-400).
  • TIFF/BigTIFF discrimination (format/tiff/): the TIFF parser now reads the magic word and immediately returns tiff.ErrUnsupportedMagic for BigTIFF (0x002B). Previously the parser would misinterpret BigTIFF offsets as TIFF-6 offsets and could seek to arbitrary positions in the file or allocate large intermediate buffers (CWE-125, CWE-400).
  • JPEG ExtendedXMP GUID cap (format/jpeg/): the parser caps distinct ExtendedXMP GUIDs at 4 per file (each GUID itself capped at 16 MiB, giving a 64 MiB aggregate ceiling); excess GUIDs are dropped and the result marked truncated, preventing memory exhaustion from crafted multi-segment JPEGs (CWE-400).
  • iptc.Encode data race eliminated (iptc/): the receiver-mutation bug described under Fixed was also a data-race vulnerability under concurrent use; the fix eliminates the race without API change (CWE-362).
  • internal/iobuf pool hardening (internal/iobuf/): the Get(n<0) panic path and the oversized-buffer pool-retention path are both closed, removing two crash/memory-exhaustion vectors reachable from attacker-controlled input sizes (CWE-400, CWE-476).

v1.0.4

08 Apr 12:34

Choose a tag to compare

Added

  • SECURITY.md: fuzz target inventory, supported fuzz targets (FuzzParseEXIF, FuzzParseIPTC, FuzzParseXMP), responsible disclosure process, and the library's security model for parser hardening.
  • CONTRIBUTING.md: full contributor guide covering dev environment setup, build and test commands, linter configuration, fuzz testing workflow, and CI pipeline overview.
  • examples/copyright-stamp: end-to-end example that reads a JPEG, sets copyright and artist metadata via EXIF and XMP, and writes the result back.
  • examples/gallery-sidecar: example that extracts metadata from any supported image format and writes an XMP sidecar file alongside the original.
  • examples/multi-format-roundtrip: example demonstrating a full read–modify–write cycle across JPEG, PNG, WebP, HEIF, and RAW formats.
  • examples/raw-inspector: example that opens RAW files (CR2, CR3, NEF, ARW, DNG, ORF, RW2) and prints all EXIF IFD entries, MakerNote fields, and GPS data.
  • examples/stream-transcode: example that streams metadata from one image format and injects it into another without loading full pixel data.
  • example_test.go: runnable Go example functions in the top-level package covering EXIF, IPTC, and XMP reading and writing across all image formats; these serve as both API documentation and tested usage samples.

Changed

  • README.md: added an Examples section with code excerpts and links to the full example programs; added benchmark reproduction instructions so contributors can verify performance claims locally.
  • Test coverage: expanded from 68% to 88% across all 25 packages. New tests target previously uncovered branches in exif/makernote (Canon, DJI, Fujifilm, Leica, Nikon, Olympus, Panasonic, Pentax, Samsung, Sigma, Sony), format (HEIF, JPEG, PNG, TIFF, WebP, all RAW variants), internal (bmff, iobuf, riff, testutil), iptc, xmp, and the top-level API (metadata_convenience_test.go, options_test.go, read_test.go).

v1.0.3

07 Apr 11:22

Choose a tag to compare

Security

  • IPTC extended-length integer overflow (iptc/iptc.go): added an immediate length < 0 guard after the extended-length accumulation loop to prevent sign-bit overflow on 32-bit platforms (IIM §1.6.2, CWE-190).
  • IPTC unbounded aggregate allocation (iptc/iptc.go): added maxIPTCTotalBytes = 256 MiB cap on the total size of all parsed datasets in a single stream, preventing memory exhaustion from crafted files with many large datasets (CWE-400).
  • XMP entity expansion (xmp/rdf.go): unescapeXML now returns an empty string and recycles the pooled builder if the decoded output of a single attribute or text node exceeds 1 MiB, preventing unbounded allocation from crafted numeric character references (CWE-776).
  • EXIF IFD entry over-allocation (exif/ifd.go): parseSingleIFD caps the pre-allocated Entries slice capacity at 1 024, preventing a crafted count = 0xFFFF field from forcing a 65 535-entry allocation before the buffer-bounds check fires (CWE-190).
  • HEIF item offset overflow (format/heif/heif.go): readItemPayload now validates that loc.offset fits in int64 before the Seek conversion, preventing sign-wrapping on the cast; added private extractItemSlice helper with the same guard for the in-memory code path.
  • PNG decompression bomb (format/png/png.go): zlibDecompress now reads through io.LimitReader capped at 64 MiB and returns a sentinel error if the limit is exceeded, preventing zip-bomb-style payloads from exhausting memory.

v1.0.2

07 Apr 10:13

Choose a tag to compare

Performance

  • XMP GPS parse: strings.Split replaced with strings.Cut; GPS coordinate parsing is now zero-allocation (BenchmarkGPSParse: 0 B/op, 0 allocs/op).
  • XMP Keywords: single-pass strings.IndexByte scan with strings.Count-pre-sized result slice replaces strings.Split; eliminates the intermediate []string allocation per call.
  • XMP AddKeyword: strings.Builder with pre-grown capacity replaces string concatenation; one allocation instead of two per keyword append.
  • XMP SetGPS: strconv.AppendFloat into a [32]byte stack buffer replaces fmt.Sprintf; eliminates heap allocation and fmt reflection overhead per GPS encode.
  • XMP writeMultiValuedProperty: strings.IndexByte loop replaces strings.Split; the []string allocation on every multi-valued property encode is eliminated.
  • XMP packet scanner: []byte("?>") literals extracted to package-level variables; no heap allocation on every Scan call.
  • XMP RDF parser: per-call []byte("-->") and []byte("?>") literals extracted to package-level; rdf:Alt item concatenation uses a pooled strings.Builder; named-entity comparison uses switch string(ref) (compiler-optimised zero-alloc path).
  • IPTC ISO-8859-1 decoder: per-call charmap.ISO8859_1.NewDecoder() replaced with a sync.Pool; decoder is Reset() before each use.
  • HEIF write path: buildIlocBox and buildMetaBox now measure required length in a first pass and allocate a single pre-sized output buffer, eliminating incremental append reallocs; appendUintN uses binary.BigEndian.AppendUint16/32/64 instead of make([]byte, n) per field.
  • JPEG segment copy: all four append([]byte(nil), ...) call sites replaced with bytes.Clone.
  • PNG write path: crc32.NewIEEE() pooled via sync.Pool to avoid per-chunk hash allocation; 8-byte chunk header stack-allocated ([8]byte instead of make([]byte, 8)).
  • PNG read path: readChunk refactored to a callback pattern; the pooled buffer is passed directly to the callback without cloning in the common (non-retained) path, saving one allocation and one copy per pass-through chunk.
  • WebP write path: bytes.Buffer in buildWebPBody pooled via sync.Pool; 4-byte RIFF chunk size field stack-allocated.
  • ORF/RW2 write path: only the 4-byte magic header is patched in-place on the io.ReadAll-owned slice; the previous full-file copy is eliminated.
  • EXIF filterEntries: accepts an extraCap argument to pre-size the result slice, avoiding a realloc when buildIFD0Entries appends trailing entries.
  • internal/bmff: Box.Equal([4]byte) bool added for zero-alloc box type comparison.
  • internal/riff: Chunk.Equal([4]byte) bool added for zero-alloc FourCC comparison.
  • XMP date layouts: inline []string literal in metadata.DateTime() hoisted to a package-level [3]string array, eliminating the per-call slice header allocation.

Changed

  • All packages now define package-level sentinel error variables (ErrXxx) for every error previously constructed inline with errors.New or fmt.Errorf; callers can now use errors.Is for reliable error identity checks. Affected packages: exif, format/heif, format/jpeg, format/png, format/tiff, format/webp, format/raw/cr3, format/raw/orf, format/raw/rw2, xmp, and the top-level package.
  • Import ordering enforced across all files (gci linter, stdlib → external → internal grouping).
  • t.Parallel() added to all table-driven tests and t.Run callbacks across all 43 test files; the entire test suite now runs with maximum parallelism under go test -race ./....
  • Linter suite expanded by five additional rules: err113 (no inline error construction), godot (comment punctuation), nestif (nesting depth ≤ 4), godox (no TODO/FIXME/HACK comments), gci (import ordering), paralleltest/tparallel (parallel test enforcement), and funlen (function length ≤ 80 lines / 60 statements).
  • metadata.DateTime() refactored from four levels of nesting to guard clauses (cyclomatic complexity reduced from 6 to 1; behaviour unchanged).

Fixed

  • sync.Pool use-after-put race in format/detect.go: mapMakeToFormat was called after tiffScanPool.Put(buf) despite makeRaw being a subslice of the pooled buffer; reordered to call mapMakeToFormat before Put.
  • sync.Pool use-after-put race in format/heif/heif.go: extractFromMetaData was called after iobuf.Put(hdrPtr) despite metaData being a subslice of the pooled buffer; reordered likewise.
  • PNG data lifetime bug: eXIf, tEXt, and iTXt chunk handlers in readChunk were retaining references to a pooled buffer slice without cloning; the callback-pattern refactor ensures retained data is always copied from the pool before the buffer is returned.

v1.0.1

06 Apr 00:17

Choose a tag to compare

Changed

  • Linter suite expanded from 25 to 46 checked rules; contributors now benefit from stricter automated enforcement including nilnesserr, wastedassign, recvcheck, inamedparam, nolintlint strict mode, intrange, mirror, modernize, and 13 additional linters.
  • All interface{} occurrences replaced with the any type alias throughout the codebase, in line with the Go 1.18+ convention.
  • All functions refactored to cyclomatic complexity ≤ 10, making the codebase easier to extend and audit.
  • CI pipeline hardened: golangci-lint v2.11.4 pinned to a specific version, all GitHub Actions runners updated to their latest major versions, gofmt -s simplification enforced on every commit, and Codecov coverage reporting integrated.
  • MIT licence file added to the repository.

Fixed

  • Variable shadowing in several parser functions: inner error variables were silently shadowing outer ones in chained binary-read paths; renamed to eliminate ambiguity (govet shadow).
  • Missing t.Helper() calls in test helper functions corrected; failure line numbers now point to the actual test case rather than the helper body.
  • Redundant strings.X(string(b), ...) patterns replaced with bytes.X(b, ...) throughout the XMP and IPTC packages, eliminating a transient allocation per call in those hot paths.
  • Several counter loops modernised to the for i := range n idiom (Go 1.22+).
  • Superfluous else blocks after early returns removed throughout the parser code.
  • Inconsistent receiver variable names within types corrected.

v1.0.0

04 Apr 11:49

Choose a tag to compare