Files
yellowjacket/.planning/research/STACK.md
T

513 lines
23 KiB
Markdown

# Technology Stack: OGG Vorbis + WAV Tag Writing
**Project:** YellowJacket v1.2.1 Format Parity
**Researched:** 2026-03-18
**Scope:** Stack additions/changes needed ONLY for OGG Vorbis and WAV tag writing
> **Context:** MP3 writing (bogem/id3v2) and FLAC writing (go-flac ecosystem) are already
> implemented and validated in v1.2. This document focuses exclusively on what's needed
> for OGG Vorbis and WAV tag writing.
---
## OGG Vorbis Tag Writing
### The Problem
OGG Vorbis stores metadata as Vorbis Comments in the second header packet of the OGG
bitstream. Modifying these comments requires:
1. Parsing OGG pages to extract the three Vorbis header packets (identification, comment, setup)
2. Decoding and modifying the Vorbis Comment packet
3. Re-encoding the modified comment packet into new OGG pages (with correct segment tables, sequence numbers, and CRC32 checksums)
4. Copying all audio data pages unchanged
5. Writing the result atomically
No pure-Go library exists that provides this end-to-end. The recommendation is a **custom OGG page rewriter** using existing building blocks.
### Option Analysis
#### Option 1: Custom OGG Page Rewriter (RECOMMENDED)
| Component | Source | Purpose |
|-----------|--------|---------|
| OGG page read/write | Custom (~300-400 LOC) | Parse OGG pages, rewrite with new comment packet |
| Vorbis Comment encode/decode | Reuse `go-flac/flacvorbis` patterns | Same binary format as FLAC Vorbis Comments |
| CRC32 computation | Custom (~30 LOC, lookup table) | OGG uses CRC32 with polynomial 0x04c11db7 |
| METADATA_BLOCK_PICTURE | Reuse `go-flac/flacpicture` | Same binary format, base64-wrapped for OGG |
**Why this is the right approach:**
- The OGG container format is simple and well-documented (27-byte header + segment table + page data)
- `jfreymuth/oggvorbis` already has a working OGG page reader in `ogg.go` (~255 LOC) that can serve as reference
- The Vorbis Comment binary format is identical to what's used in FLAC — the existing `flacvorbis` library's comment manipulation code can be reused or adapted
- Audio data pages are copied byte-for-byte — no audio re-encoding
- Full control over the implementation with no external dependency risk
**Implementation complexity:** MEDIUM. The OGG page format has exactly one tricky aspect:
packets can span multiple pages (via the continuation-of-packet flag and lacing values).
The comment packet is typically small enough to fit in one page, but the implementation
must handle the general case (especially when large cover art is embedded, inflating the
comment packet well beyond one page's ~65KB limit).
**Estimated LOC:** ~500-600 for a complete `ogg.go` writer in the tagwriter package.
#### Option 2: `mccoyst/ogg` for OGG Page Encoding
| Technology | Version | Stars | Purpose |
|------------|---------|-------|---------|
| `github.com/mccoyst/ogg` | latest (no semver tags) | 37 | OGG page encode/decode |
**What it provides:**
- `ogg.Decoder` — reads OGG pages, extracts packets
- `ogg.Encoder` — writes packets into OGG pages with correct framing, CRC, segment tables
- `EncodeBOS()`, `Encode()`, `EncodeEOS()` — page-type-aware encoding
- Handles packet splitting across pages automatically
- 100% test coverage claimed, 52 commits, MIT license
**Assessment:** This is the strongest external option. The Encoder handles the hardest parts
(lacing values, page splitting, CRC). However:
- **No semver releases** — importing is `github.com/mccoyst/ogg` with no version guarantee
- **37 stars, 1 open issue** — small community, low bus factor
- **Still requires Vorbis-specific logic** — mccoyst/ogg handles OGG pages but knows nothing about Vorbis header packets. We'd still need to:
- Identify and extract the three Vorbis header packets
- Parse/modify the Vorbis Comment packet
- Handle the `\x03vorbis` packet type prefix
- Re-encode everything in the correct order
**Verdict:** POSSIBLE but the marginal benefit over custom code is small. The OGG page format
is well-specified; the Encoder's value is mainly in lacing value calculation and CRC, which
are ~80 LOC total. Adding a dependency for ~80 LOC of saved work introduces a maintenance
and stability risk for a library with no tagged releases.
**Recommendation: Custom implementation.** If OGG page writing proves harder than expected
during implementation, `mccoyst/ogg` can be pulled in as a fallback.
#### Option 3: `jfreymuth/oggvorbis` (Already a Dependency)
| Technology | Version | Stars | Purpose |
|------------|---------|-------|---------|
| `github.com/jfreymuth/oggvorbis` | v1.0.5 | 75 | OGG Vorbis decoder |
**Assessment:** Decode-only. No write API. The `ogg.go` internal reader is useful as
**reference code** for understanding OGG page parsing, but the types are unexported
and the package provides no page-level write capability.
**Verdict:** NOT SUITABLE for writing. Useful as reference only.
#### Option 4: `dhowden/tag` Write Support
**Assessment:** dhowden/tag is read-only (642 stars, no write API). Its OGG parsing is
minimal — it extracts Vorbis Comments for reading but does not provide any mechanism
to modify or write back. Forking would require building the entire OGG page rewriter
anyway, plus inheriting maintenance burden.
**Verdict:** NOT SUITABLE.
#### Option 5: External CLI Tools (vorbiscomment, ffmpeg)
| Tool | What It Does | Issue |
|------|-------------|-------|
| `vorbiscomment` | CLI for reading/writing Vorbis Comments in OGG | Requires packaging/distributing a C binary |
| `ffmpeg` | Swiss-army multimedia tool | Massive binary (~100MB), CGo-equivalent burden |
| `opustags` | Opus metadata editor | Wrong codec (Opus, not Vorbis) |
**Assessment:** Shelling out to external CLI tools would work but violates the pure-Go
spirit of the project. It introduces:
- Distribution complexity (packaging native binaries per platform)
- Runtime dependency management (checking tool availability, version compat)
- Error handling complexity (parsing CLI output)
- No Windows/macOS availability guarantee without extra bundling
**Verdict:** NOT RECOMMENDED. Only consider as absolute last resort if custom Go
implementation proves infeasible (it won't — the format is well-understood).
### OGG Vorbis Writing: Recommended Stack
| Component | Approach | New Dependency? |
|-----------|----------|-----------------|
| OGG page parsing | Custom `oggRewriter` in tagwriter package | No |
| OGG page writing | Custom (header + segment table + CRC32) | No |
| Vorbis Comment manipulation | Reuse `flacvorbis` patterns + shared helpers | No (already a dep) |
| Cover art (METADATA_BLOCK_PICTURE) | `go-flac/flacpicture` for binary encoding + base64 wrapper | No (already a dep) |
| Atomic file write | Existing `fileutil.AtomicWrite` | No |
**Total new dependencies: ZERO.** Everything needed is either already in the dep tree
or implementable from well-documented specifications.
### OGG Vorbis Cover Art
**YES — OGG Vorbis can embed cover art.**
Cover art in OGG Vorbis uses the `METADATA_BLOCK_PICTURE` Vorbis Comment field:
1. Create a FLAC Picture binary block (same as used in FLAC files):
- Picture type (3 = front cover)
- MIME type string ("image/jpeg" or "image/png")
- Description string ("Front cover")
- Width, height, color depth, palette size (can be set to 0)
- Raw image data
2. Base64-encode the entire binary block
3. Add as Vorbis Comment: `METADATA_BLOCK_PICTURE=<base64 data>`
**Integration with existing code:** The `go-flac/flacpicture` library already creates
the binary FLAC Picture block. For FLAC files, this block goes into a metadata block.
For OGG files, the same binary block gets base64-encoded and inserted as a Vorbis
Comment string. We can reuse `flacpicture.NewFromImageData()` and add a base64 wrapper.
**Confidence:** HIGH — This is the official Xiph.org recommendation per
https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
### OGG Vorbis Format Details (Implementation Reference)
**OGG Page Structure (27 bytes header):**
```
Bytes 0-3: "OggS" capture pattern
Byte 4: Stream structure version (always 0)
Byte 5: Header type flag (0x01=continued, 0x02=BOS, 0x04=EOS)
Bytes 6-13: Absolute granule position (int64, little-endian)
Bytes 14-17: Stream serial number (uint32, little-endian)
Bytes 18-21: Page sequence number (uint32, little-endian)
Bytes 22-25: CRC32 checksum (computed with this field zeroed)
Byte 26: Number of segments (0-255)
Bytes 27+: Segment table (one byte per segment, lacing values)
Page data follows immediately
```
**Vorbis Header Packets in OGG:**
- Packet 1: Identification header (starts with `\x01vorbis`)
- Packet 2: Comment header (starts with `\x03vorbis`) ← THIS IS WHAT WE MODIFY
- Packet 3: Setup header (starts with `\x05vorbis`)
- Packets 4+: Audio data
**Vorbis Comment Binary Format (within packet 2, after `\x03vorbis` prefix):**
```
[vendor_length: uint32 LE] [vendor_string: bytes]
[comment_count: uint32 LE]
for each comment:
[length: uint32 LE] [comment: bytes] // e.g. "ARTIST=Bob Dylan"
[framing_bit: 1 bit, must be 1]
```
**Algorithm for tag writing:**
1. Read all OGG pages from source file
2. Extract packets 1, 2, 3 from the first few pages (header pages)
3. Parse Vorbis Comment from packet 2 (skip `\x03vorbis` prefix)
4. Modify comments (add/replace/remove fields)
5. Re-serialize Vorbis Comment packet (with `\x03vorbis` prefix)
6. Write to temp file via AtomicWrite:
a. Write packet 1 (identification) as BOS page
b. Write modified packet 2 (comment) + packet 3 (setup) as continuation pages
c. Copy all remaining audio pages, re-sequencing page numbers
7. Atomic rename over original
---
## WAV Tag Writing
### The Problem
WAV files use the RIFF container format. Metadata in WAV files can be stored in:
1. **RIFF INFO chunks** (`LIST`/`INFO` sub-chunks like `IART`, `INAM`, `IPRD`) — the oldest and most widely supported mechanism
2. **ID3v2 chunks** (`id3 ` RIFF chunk containing a full ID3v2 tag) — newer, more expressive, used by some modern tools
3. **BEXT chunks** (Broadcast Wave Extension) — professional/broadcast use only
The challenge is that no dominant pure-Go library exists for WAV metadata writing, and the ecosystem recently lost its most popular option.
### Option Analysis
#### Option 1: Custom RIFF Chunk Writer for LIST/INFO (RECOMMENDED)
| Component | Source | Purpose |
|-----------|--------|---------|
| RIFF chunk reader | Custom (~200 LOC) | Parse WAV RIFF structure, find/modify LIST INFO chunk |
| INFO field writing | Custom (~150 LOC) | Write INFO sub-chunks with metadata |
| Atomic file write | Existing `fileutil.AtomicWrite` | Crash-safe file operations |
**RIFF INFO Chunk Field Mapping:**
| YellowJacket Field | INFO Chunk ID | Description |
|--------------------|---------------|-------------|
| Title | `INAM` | Name/title of the work |
| Artist | `IART` | Artist name |
| Album | `IPRD` | Product/album name |
| Genre | `IGNR` | Genre |
| Year | `ICRD` | Creation date |
| Track Number | `ITRK` | Track number |
| Composer | `IMUS` | Composer/music by |
| Comment | `ICMT` | Comment |
**Why RIFF INFO over ID3v2-in-WAV:**
- RIFF INFO is the native WAV metadata format — it's part of the RIFF specification
- Universal player support (Windows Media Player, VLC, foobar2000, etc.)
- Simple format: 4-byte chunk ID + 4-byte size + null-terminated string
- dhowden/tag already reads RIFF INFO chunks, so round-trip works
- No additional dependencies needed
**Why NOT ID3v2-in-WAV:**
- The `id3 ` chunk is a de facto standard, not an official RIFF spec feature
- Not all players/tools support it
- dhowden/tag reads ID3v2 in WAV (it detects it), but our existing `bogem/id3v2` expects MP3 file structure (it calls `tag.Open()` which reads from position 0, expecting an ID3v2 header). Using bogem/id3v2 for WAV would require significant adaptation to handle the RIFF wrapper.
- More complex: we'd need to create an ID3v2 tag, serialize it, then embed it as a RIFF chunk
**Implementation approach:**
1. Read entire WAV file (parse RIFF chunks: `RIFF`, `fmt `, `data`, `LIST`, etc.)
2. Find or create `LIST`/`INFO` chunk
3. Write/replace INFO sub-chunks with new metadata values
4. Reassemble file: RIFF header → fmt chunk → data chunk → LIST/INFO chunk → other chunks
5. Update RIFF header size
6. Write via AtomicWrite
**Estimated LOC:** ~300-400 for RIFF INFO reading/writing.
#### Option 2: `go-audio/wav` + `go-audio/riff`
| Technology | Status | Stars |
|------------|--------|-------|
| `github.com/go-audio/wav` | **ARCHIVED Feb 21, 2026** | 383 |
| `github.com/go-audio/riff` | **ARCHIVED Feb 21, 2026** | 11 |
**Assessment:** Both libraries were archived less than one month ago. They provided WAV
encoding/decoding and RIFF chunk parsing, but:
- `go-audio/wav` is focused on audio encoding/decoding, not metadata manipulation
- `go-audio/riff` has only a parser (3 commits total), no writer
- Neither supports writing RIFF INFO metadata
- Both are now unmaintained/archived — using them would be a dead-end dependency
**Verdict:** NOT SUITABLE. Archived, no metadata write support.
#### Option 3: `bogem/id3v2` for ID3v2-in-WAV
**Assessment:** bogem/id3v2's `Tag.WriteTo(w io.Writer)` writes raw ID3v2 tag bytes to
any writer. In theory, we could:
1. Create an ID3v2 tag with bogem/id3v2
2. Serialize it to bytes via `WriteTo()`
3. Wrap it in a RIFF `id3 ` chunk
4. Insert the chunk into the WAV file
This is technically feasible but:
- Still requires custom RIFF chunk manipulation code
- ID3v2-in-WAV has worse player compatibility than RIFF INFO
- More complex than just writing RIFF INFO chunks directly
- `tag.Open("file.wav")` and `tag.Save()` won't work — those expect MP3 file structure
**Verdict:** NOT RECOMMENDED as primary approach. Could be added later as an enhancement
for richer metadata (cover art via APIC), but RIFF INFO should be the primary mechanism.
#### Option 4: External CLI Tools (ffmpeg, exiftool)
Same issues as OGG: distribution complexity, runtime dependencies, error handling overhead.
**Verdict:** NOT RECOMMENDED.
### WAV Writing: Recommended Stack
| Component | Approach | New Dependency? |
|-----------|----------|-----------------|
| RIFF container parsing | Custom RIFF reader in tagwriter package | No |
| RIFF INFO chunk writing | Custom (4-byte IDs + null-terminated strings) | No |
| Atomic file write | Existing `fileutil.AtomicWrite` | No |
**Total new dependencies: ZERO.**
### WAV Cover Art
**RIFF INFO: NO — cannot embed cover art.**
RIFF INFO chunks are limited to simple text key-value pairs. There is no standard
INFO sub-chunk for binary image data.
**ID3v2-in-WAV: YES — via APIC frame.**
If an `id3 ` RIFF chunk is present (or added), it can contain a full ID3v2 tag with
APIC (Attached Picture) frames, just like MP3 files.
**Recommendation for v1.2.1:** Do NOT implement WAV cover art. The RIFF INFO approach
gives us text tag support with zero dependencies, and WAV cover art support is rare
in the wild. Cover art for WAV can be deferred to a future enhancement using the
ID3v2-in-WAV approach if there's user demand.
### WAV RIFF Format Details (Implementation Reference)
**RIFF File Structure:**
```
"RIFF" [file_size: uint32 LE] "WAVE"
"fmt " [chunk_size: uint32 LE] [format data...]
"data" [chunk_size: uint32 LE] [audio samples...]
"LIST" [chunk_size: uint32 LE] "INFO"
"INAM" [size: uint32 LE] "Track Title\0"
"IART" [size: uint32 LE] "Artist Name\0"
...
```
**Key implementation details:**
- All integers are little-endian (unlike OGG which is a mix)
- Chunk sizes must be even (pad with 0x00 byte if odd)
- Strings in INFO chunks are null-terminated
- The RIFF header size field = total file size - 8
- LIST/INFO chunk can appear anywhere after `fmt ` and `data`
- Multiple LIST chunks may exist; only `LIST`/`INFO` contains metadata
**Algorithm for tag writing:**
1. Parse RIFF chunks by reading 4-byte ID + 4-byte size pairs
2. Collect all chunks, preserving order
3. Find or create LIST/INFO chunk
4. Replace/add INFO sub-chunks for changed fields
5. Reassemble file via AtomicWrite:
a. Write RIFF header with updated total size
b. Write fmt chunk (unchanged)
c. Write data chunk (unchanged — just copy bytes)
d. Write LIST/INFO chunk with metadata
e. Write any other chunks (unchanged)
6. Atomic rename
---
## Integration with Existing Pipeline
### Format Detection Changes
Current `tagwriter.DetectFormat()` supports MP3 and FLAC. Add OGG and WAV:
```go
const (
FormatMP3 AudioFormat = "mp3"
FormatFLAC AudioFormat = "flac"
FormatOGG AudioFormat = "ogg" // NEW
FormatWAV AudioFormat = "wav" // NEW
)
func DetectFormat(filePath string) (AudioFormat, error) {
switch strings.ToLower(filepath.Ext(filePath)) {
case ".mp3": return FormatMP3, nil
case ".flac": return FormatFLAC, nil
case ".ogg": return FormatOGG, nil // NEW
case ".wav": return FormatWAV, nil // NEW
default: return "", errUnsupportedFormat
}
}
```
### Pipeline Dispatch Changes
Current `WriteTrackTags()` switch in `pipeline.go`:
```go
switch format {
case FormatMP3: err = writeMp3Tags(tw.logger, audioFile.FilePath, changes)
case FormatFLAC: err = writeFlacTags(tw.logger, audioFile.FilePath, changes)
case FormatOGG: err = writeOggTags(tw.logger, audioFile.FilePath, changes) // NEW
case FormatWAV: err = writeWavTags(tw.logger, audioFile.FilePath, changes) // NEW
}
```
### Shared Code Reuse
**Vorbis Comment helpers** (already exist in `flac.go`):
- `replaceVorbisComment()` — remove existing field, add new value
- `applyFlacTextChanges()` — map TagChanges to Vorbis Comment fields
- Field mapping constants (`FIELD_TITLE`, `FIELD_ARTIST`, etc.)
These should be **extracted to a shared file** (e.g., `vorbis_comments.go`) and reused
by both `flac.go` and the new `ogg.go`. The logic is identical — both formats use Vorbis
Comments with the same field names.
**Cover art helpers** (already exist in `tagwriter.go`):
- `detectMIME()` — determine image MIME type from magic bytes
- `asBytes()` — extract byte slice from TagChanges value
**AtomicWrite** (already exists in `fileutil/atomicwrite.go`):
- Used identically for all formats: write to temp → sync → rename
---
## Dependency Summary
### New Dependencies Required
**NONE.** Both OGG Vorbis and WAV tag writing are implemented as custom code in the
tagwriter package, using only:
- Go standard library (`encoding/binary`, `encoding/base64`, `bytes`, `io`, `os`)
- Existing dependencies (`go-flac/flacpicture` for METADATA_BLOCK_PICTURE binary format)
- Existing utilities (`fileutil.AtomicWrite`)
### Existing Dependencies Unchanged
| Library | Current Use | Use in v1.2.1 |
|---------|------------|---------------|
| `dhowden/tag` v0.0.0-20240417 | Tag reading (all formats) | Unchanged — validates OGG/WAV round-trip |
| `go-flac/flacvorbis/v2` v2.0.2 | FLAC Vorbis Comment manipulation | Shared patterns for OGG comment encoding |
| `go-flac/flacpicture/v2` v2.0.2 | FLAC PICTURE block creation | Reused for OGG METADATA_BLOCK_PICTURE |
| `go-flac/go-flac/v2` v2.0.4 | FLAC metadata block manipulation | Unchanged |
| `bogem/id3v2/v2` v2.1.4 | MP3 ID3v2 tag writing | Unchanged |
| `jfreymuth/oggvorbis` v1.0.5 | OGG Vorbis decoding (via beep) | Reference for OGG page structure |
---
## What NOT To Add
| Library / Approach | Why Avoid |
|--------------------|-----------|
| `mccoyst/ogg` | No semver releases, 37 stars. The OGG page format is simple enough to implement directly (~80 LOC for the encoding part). Avoids dependency risk for minimal gain. |
| `go-audio/wav` or `go-audio/riff` | **Archived Feb 21, 2026.** Do not depend on abandoned libraries. |
| Any CGo-based library (taglib-go, etc.) | Violates pure-Go constraint |
| `bogem/id3v2` for WAV files | Its `Open()`/`Save()` API expects MP3 file structure. Would need significant wrapping for RIFF container. RIFF INFO is simpler and more compatible. |
| ID3v2-in-WAV for v1.2.1 | Adds complexity for marginal benefit. RIFF INFO covers the primary use case. Defer ID3v2-in-WAV to a future milestone if cover art in WAV is needed. |
| External CLI tools (vorbiscomment, ffmpeg) | Distribution complexity, runtime deps, violates pure-Go spirit |
---
## Format Coverage Matrix (v1.2.1 Target)
| Format | Text Tags | Cover Art | Approach | Complexity | Confidence |
|--------|-----------|-----------|----------|------------|------------|
| MP3 (ID3v2) | ✓ All fields | ✓ APIC frame | bogem/id3v2 (existing) | Done | HIGH |
| FLAC | ✓ All fields | ✓ PICTURE block | go-flac ecosystem (existing) | Done | HIGH |
| OGG Vorbis | ✓ All fields | ✓ METADATA_BLOCK_PICTURE | Custom OGG page rewriter | Medium | MEDIUM-HIGH |
| WAV | ✓ Core fields | ✗ Not in v1.2.1 | Custom RIFF INFO writer | Low-Medium | MEDIUM-HIGH |
**"Core fields" for WAV:** Title, Artist, Album, Genre, Year, Track Number, Composer.
Album Artist and Disc Number have no standard RIFF INFO chunk IDs. They can be omitted
or stored in non-standard INFO chunks if needed.
---
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| OGG page splitting with large cover art | Medium | Medium | Test with cover art >64KB (forces multi-page comment packet) |
| WAV files with unusual RIFF chunk ordering | Low | Low | Parse all chunks generically, preserve unknown chunks |
| dhowden/tag can't read what we write (OGG) | Low | High | Round-trip tests: write with custom writer, read with dhowden/tag |
| dhowden/tag can't read what we write (WAV) | Low | High | Round-trip tests for RIFF INFO chunks |
| OGG CRC32 computation mismatch | Low | High | Use the exact polynomial from OGG spec (0x04c11db7), test against known files |
| WAV INFO chunk padding errors | Medium | Low | RIFF spec requires even-aligned chunks; easy to forget padding byte |
---
## Sources
### Official Specifications (HIGH confidence)
- OGG framing: https://xiph.org/ogg/doc/framing.html
- OGG RFC: https://xiph.org/ogg/doc/rfc3533.txt
- Vorbis I Spec (Section 5 — comment field): https://xiph.org/vorbis/doc/Vorbis_I_spec.html
- Vorbis Comment spec: https://xiph.org/vorbis/doc/v-comment.html
- METADATA_BLOCK_PICTURE: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
- FLAC Picture block format: https://xiph.org/flac/format.html#metadata_block_picture
### Libraries Evaluated (HIGH confidence — GitHub repos)
- jfreymuth/oggvorbis: https://github.com/jfreymuth/oggvorbis (75 stars, decode-only)
- mccoyst/ogg: https://github.com/mccoyst/ogg (37 stars, encode+decode, no semver)
- dhowden/tag: https://github.com/dhowden/tag (642 stars, read-only)
- go-audio/wav: https://github.com/go-audio/wav (383 stars, **ARCHIVED 2026-02-21**)
- go-audio/riff: https://github.com/go-audio/riff (11 stars, **ARCHIVED 2026-02-21**)
- bogem/id3v2: https://github.com/n10v/id3v2 (359 stars, MP3-focused API)
### Existing Codebase (HIGH confidence — already validated in v1.2)
- `backend/tagwriter/flac.go` — Vorbis Comment manipulation patterns
- `backend/tagwriter/mp3.go` — AtomicWrite integration pattern
- `backend/tagwriter/tagwriter.go` — TagChanges, format detection, helper functions
- `backend/fileutil/atomicwrite.go` — Crash-safe file write utility