Releases: FlavioCFOliveira/GoMetadata
v1.2.0
[1.2.0] - 2026-06-10
Added
-
Write support for all 13 container formats (
write.go,format/format.go):format.SupportsWritenow returnstruefor every supported format. The only remaining write limitation is BigTIFF:WriteandWriteFilereturnErrWriteNotSupportedwhen the source is a BigTIFF file (magic0x002B). -
TIFF copy-and-relocate metadata write (
format/tiff/, #92/#93):tiff.Injectuses a copy-and-relocate serialiser (format/tiff/relocate.go) that enumerates every image-data block referenced byStripOffsets(0x0111),TileOffsets(0x0144), andJPEGInterchangeFormat(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 errorstiff.ErrBlockOutOfBounds,tiff.ErrUnsupportedOffsetType,tiff.ErrTruncatedOffsetArray, andtiff.ErrUnsupportedElemSizeare exported for diagnostic use. -
DNG SubIFD recursive relocation and write support (
format/tiff/relocate.go, #94 + #98): the copy-and-relocate serialiser recursively followsSubIFDs(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 theirvalOrOffpointers updated to the new absolute positions, preventingXResolution/YResolutionand similar fields from becoming undefined after write. Validated against a real Pentax QS1 DNG corpus file:ImageDataHashIN==OUT. Sentinel errorstiff.ErrSubIFDPointerArrayOOBandtiff.ErrSubIFDEntryNotFoundare 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 samewriteTIFFcopy-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:ImageDataHashIN==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
PreviewIFDandNikonScanIFD(which live beyond the declared byte count in the outer TIFF entry), enumerates the PreviewIFD image block, and patches the MakerNote-relative0x0201offset after re-encoding. Validated against a real Nikon D70 NEF corpus file:ImageDataHashIN==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:
ImageDataHashIN==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:
ImageDataHashIN==OUT. -
CR3 metadata write with
stco/co64offset relocation (format/raw/cr3/, #91):cr3.Injectrebuilds the Canon UUID box with the new CMTx payloads, then walks everytrak → mdia → minf → stbl → {stco, co64}table inside the rebuiltmoov, addingdeltato each chunk offset pointing at or beyond the originalmoovend.stcooverflow (relocated value >MaxUint32) returnscr3.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;WriteandWriteFilereturnErrWriteNotSupportedfor 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 indocs/conformance/(one file per spec family:exif-tiff.md,iptc.md,xmp.md,containers.md). -
cr3.ErrNoCMT1Box: new exported sentinel returned bycr3.Extractwhen the Canon UUID structure is present but contains no CMT1 sub-box. The top-levelReadconverts 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.SupportsWritefor TIFF, DNG, CR2, NEF, ARW, ORF, RW2, CR3: all now returntrue. Each format has a dedicated write path with real-corpus validation. The only remaining write limitation is BigTIFF (returnsErrWriteNotSupported).- BigTIFF write:
WriteandWriteFilereturnErrWriteNotSupportedwhen the source is a BigTIFF file (magic0x002B). Useformat.SupportsWrite(id)to check write capability programmatically. Writecross-format mismatch detection (#108):Writenow returnsErrFormatMismatchwhen the*Metadatawas read from a different container format than the write target, preventing silent data loss from mismatched roundtrips.Writeidempotency (#109): callingWriteon an already-written byte slice now produces identical output. The fix eliminates a class of bugs where repeated writes caused progressive format divergence.WriteFileatomicity and safety (#124/#125):WriteFilenow 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.Encodenow emits datasets in ascending record-and-dataset-number order as required by IIM §7. Duplicate Dataset 1:00EnvelopeRecordVersionheaders 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 EXIFGPSnamespace, allowing GPS coordinates stored by W3C-Geo-aware XMP producers to be decoded correctly. PreserveUnknownSegmentsoption honoured (#85): thePreserveUnknownSegments(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/metaboxes (#106/#169/#177):parseInfeV0V1now bounds-checks the item protection index read before advancing the position pointer.buildInjectComponentsvalidatesmetaContentOff <= metaAbsEndbefore slicing, closing thepanic: slice bounds out of rangepaths that were confirmed by the fuzzer. AVIF brand detection hardened. - HEIF
ilocconstruction method misresolution (#133/#137):parseIlocItemSimplenow reads and checksconstruction_method; items withconstruction_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...
v1.1.0
[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 witherrors.Is(err, tiff.ErrUnsupportedMagic).xmp.ErrDocumentTooLarge: new exported sentinel error returned when an XMP document exceeds themaxXMPDocumentBytesinput cap (16 MiB, compile-time constant). Callers can useerrors.Isto distinguish this condition from malformed-XML errors.FuzzRead: end-to-end fuzz target at the top-level package (FuzzRead) that drives the fullReadorchestrator with arbitrary input. This joins 26 existing fuzz targets for a total of 27.- CI fuzz job: a new
fuzzCI 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
FormatCapabilitymatrix mirroringformat.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.gonow precisely state thatWriteis supported for JPEG, PNG, WebP, HEIF/AVIF, and Canon CR3. TIFF-based containers (TIFF, CR2, NEF, ARW, DNG, ORF, RW2) are read-only;WritereturnsErrWriteNotSupportedfor those formats. go.mod:golang.org/x/textreclassified as a direct dependency (the// indirectannotation removed bygo mod tidy).- Test coverage: +207 tests added since v1.0.4. New tests cover EXIF/TIFF adversarial fuzz seeds, the
internal/iobufbuffer pool (race, contamination, and DoS scenarios), and the top-level read orchestrator.
Fixed
iptc.Encodereceiver mutation (iptc/iptc.go):Encodepreviously appended the IPTC 1:90 UTF-8CodedCharacterSetmarker directly to the receiver'sRecords[0]slice when emitting non-ASCII content, mutating shared state and creating a data race under concurrentWritecalls. The fix emits the 1:90 declaration to the encoded output only (via the existingneedsUTF8Declarationpath); the receiver is now pure and idempotent.internal/iobufpool hardening (internal/iobuf/iobuf.go):Get(n)now clamps negativento zero instead of passing it tomake, which would have panicked.Putnow 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/): introducedmaxXMPDocumentBytes(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 returnsxmp.ErrDocumentTooLargewithout 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 returnstiff.ErrUnsupportedMagicfor 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.Encodedata 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/iobufpool hardening (internal/iobuf/): theGet(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
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
Security
- IPTC extended-length integer overflow (
iptc/iptc.go): added an immediatelength < 0guard 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): addedmaxIPTCTotalBytes = 256 MiBcap 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):unescapeXMLnow 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):parseSingleIFDcaps the pre-allocatedEntriesslice capacity at 1 024, preventing a craftedcount = 0xFFFFfield from forcing a 65 535-entry allocation before the buffer-bounds check fires (CWE-190). - HEIF item offset overflow (
format/heif/heif.go):readItemPayloadnow validates thatloc.offsetfits inint64before theSeekconversion, preventing sign-wrapping on the cast; added privateextractItemSlicehelper with the same guard for the in-memory code path. - PNG decompression bomb (
format/png/png.go):zlibDecompressnow reads throughio.LimitReadercapped at 64 MiB and returns a sentinel error if the limit is exceeded, preventing zip-bomb-style payloads from exhausting memory.
v1.0.2
Performance
- XMP GPS parse:
strings.Splitreplaced withstrings.Cut; GPS coordinate parsing is now zero-allocation (BenchmarkGPSParse: 0 B/op, 0 allocs/op). - XMP
Keywords: single-passstrings.IndexBytescan withstrings.Count-pre-sized result slice replacesstrings.Split; eliminates the intermediate[]stringallocation per call. - XMP
AddKeyword:strings.Builderwith pre-grown capacity replaces string concatenation; one allocation instead of two per keyword append. - XMP
SetGPS:strconv.AppendFloatinto a[32]bytestack buffer replacesfmt.Sprintf; eliminates heap allocation andfmtreflection overhead per GPS encode. - XMP
writeMultiValuedProperty:strings.IndexByteloop replacesstrings.Split; the[]stringallocation on every multi-valued property encode is eliminated. - XMP packet scanner:
[]byte("?>")literals extracted to package-level variables; no heap allocation on everyScancall. - XMP RDF parser: per-call
[]byte("-->")and[]byte("?>")literals extracted to package-level;rdf:Altitem concatenation uses a pooledstrings.Builder; named-entity comparison usesswitch string(ref)(compiler-optimised zero-alloc path). - IPTC ISO-8859-1 decoder: per-call
charmap.ISO8859_1.NewDecoder()replaced with async.Pool; decoder isReset()before each use. - HEIF write path:
buildIlocBoxandbuildMetaBoxnow measure required length in a first pass and allocate a single pre-sized output buffer, eliminating incrementalappendreallocs;appendUintNusesbinary.BigEndian.AppendUint16/32/64instead ofmake([]byte, n)per field. - JPEG segment copy: all four
append([]byte(nil), ...)call sites replaced withbytes.Clone. - PNG write path:
crc32.NewIEEE()pooled viasync.Poolto avoid per-chunk hash allocation; 8-byte chunk header stack-allocated ([8]byteinstead ofmake([]byte, 8)). - PNG read path:
readChunkrefactored 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.BufferinbuildWebPBodypooled viasync.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 anextraCapargument to pre-size the result slice, avoiding a realloc whenbuildIFD0Entriesappends trailing entries. internal/bmff:Box.Equal([4]byte) booladded for zero-alloc box type comparison.internal/riff:Chunk.Equal([4]byte) booladded for zero-alloc FourCC comparison.- XMP date layouts: inline
[]stringliteral inmetadata.DateTime()hoisted to a package-level[3]stringarray, eliminating the per-call slice header allocation.
Changed
- All packages now define package-level sentinel error variables (
ErrXxx) for every error previously constructed inline witherrors.Neworfmt.Errorf; callers can now useerrors.Isfor 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 (
gcilinter, stdlib → external → internal grouping). t.Parallel()added to all table-driven tests andt.Runcallbacks across all 43 test files; the entire test suite now runs with maximum parallelism undergo 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), andfunlen(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.Pooluse-after-put race informat/detect.go:mapMakeToFormatwas called aftertiffScanPool.Put(buf)despitemakeRawbeing a subslice of the pooled buffer; reordered to callmapMakeToFormatbeforePut.sync.Pooluse-after-put race informat/heif/heif.go:extractFromMetaDatawas called afteriobuf.Put(hdrPtr)despitemetaDatabeing a subslice of the pooled buffer; reordered likewise.- PNG data lifetime bug:
eXIf,tEXt, andiTXtchunk handlers inreadChunkwere 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
Changed
- Linter suite expanded from 25 to 46 checked rules; contributors now benefit from stricter automated enforcement including
nilnesserr,wastedassign,recvcheck,inamedparam,nolintlintstrict mode,intrange,mirror,modernize, and 13 additional linters. - All
interface{}occurrences replaced with theanytype 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 -ssimplification 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 withbytes.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 nidiom (Go 1.22+). - Superfluous
elseblocks after early returns removed throughout the parser code. - Inconsistent receiver variable names within types corrected.
v1.0.0
Full Changelog: https://github.com/FlavioCFOliveira/GoMetadata/commits/v1.0.0