Skip to content

feat(sdk): add opt-in unprefixed manifest entry name - #4049

Open
pflynn-virtru wants to merge 1 commit into
mainfrom
fix/3513-manifest-json-reader-tolerance
Open

pflynn-virtru wants to merge 1 commit into
mainfrom
fix/3513-manifest-json-reader-tolerance

Conversation

@pflynn-virtru

@pflynn-virtru pflynn-virtru commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

Proposed Changes

Adds an opt-in that writes the TDF manifest under the zip entry name manifest.json, the name the TDF spec defines, instead of the legacy 0.manifest.json. Default output is unchanged, byte for byte.

  • WithUnprefixedManifestName() (CreateTDF), WithChunkedUnprefixedManifestName() (chunked writer), and WithUnprefixedManifestName() in sdk/experimental/tdf. The names describe the format they produce, matching WithHexlessPolicyBinding (feat(sdk): add opt-in hexless policy binding #4232).
  • A pre-4.3.0 WithTargetMode / WithChunkedTargetMode overrides the opt-in and keeps 0.manifest.json, whatever the option order.
  • Readers already accept both names (fix(sdk): read the TDF manifest under the spec name manifest.json #4063). Readers released before that (Go SDK releases before v0.35.0) can't open output written with this option; the godoc says so.
  • The archive-writer factory now forwards zipstream options, so one decision point serves CreateTDF, the chunked writer and the experimental writer.

This replaces the earlier version of this PR, which changed the default and was a breaking change. Making manifest.json the default remains a separate, later change.

Refs #3513.

Checklist

Testing Instructions

  • cd sdk && go test ./... -race
  • golangci-lint run --new-from-rev=origin/main ./... in sdk: 0 new issues.
  • New tests: TestSegmentWriter_ManifestEntryName, TDFSuite.Test_ManifestEntryName, TestChunkedManifestEntryName, and the experimental ManifestEntryName case. Each covers the default, the opt-in, and a legacy target mode in both option orders, and round-trips through LoadTDF.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • TDF writers can now optionally store the manifest as manifest.json instead of the default 0.manifest.json.
    • The option is available for standard, chunked, and experimental writers. Legacy target modes continue to use 0.manifest.json, regardless of option order.

@pflynn-virtru
pflynn-virtru requested review from a team as code owners September 15, 2026 14:58
@github-actions github-actions Bot added the comp:sdk A software development kit, including library, for client applications and inter-service communicati label Sep 15, 2026
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 19c1e817-a759-4833-a594-8389d7b2136f
📥 Commits

Reviewing files that changed from the base of the PR and between 094d60e and 4ec7aa7.

📒 Files selected for processing: 14
  • sdk/chunked_options.go
  • sdk/chunked_test.go
  • sdk/chunked_writer.go
  • sdk/experimental/tdf/manifest.go
  • sdk/experimental/tdf/options.go
  • sdk/experimental/tdf/writer.go
  • sdk/experimental/tdf/writer_test.go
  • sdk/internal/zipstream/segment_writer.go
  • sdk/internal/zipstream/segment_writer_test.go
  • sdk/internal/zipstream/writer.go
  • sdk/internal/zipstream/zip_headers.go
  • sdk/tdf.go
  • sdk/tdf_config.go
  • sdk/tdf_test.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The PR adds an option to write the manifest as manifest.json. The default remains 0.manifest.json. Stable, chunked, and experimental SDK writer paths pass the option through to zipstream, and tests cover both names and legacy target-mode behavior.

Changes: Manifest entry naming

Layer / File(s) Summary
zipstream manifest emission
sdk/internal/zipstream/writer.go, sdk/internal/zipstream/zip_headers.go, sdk/internal/zipstream/segment_writer.go, sdk/internal/zipstream/segment_writer_test.go
zipstream adds UnprefixedManifestName and its option. segmentWriter.Finalize emits manifest.json when enabled; otherwise, it emits 0.manifest.json.
SDK option and writer plumbing
sdk/tdf_config.go, sdk/chunked_options.go, sdk/chunked_writer.go, sdk/tdf.go, sdk/experimental/tdf/options.go, sdk/experimental/tdf/writer.go, sdk/experimental/tdf/manifest.go
Stable, chunked, and experimental SDK options pass the name setting through to zipstream. The chunked writer does not pass the option when useHex is enabled.
Manifest name validation
sdk/tdf_test.go, sdk/chunked_test.go, sdk/experimental/tdf/writer_test.go
Tests check the default and unprefixed names, plus legacy target mode with the option in either order. Chunked test factories forward ZIP options.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant TDFConfig
  participant newTDFChunkedWriter
  participant newChunkedWriter
  participant segmentWriter.Finalize
  TDFConfig->>newTDFChunkedWriter: pass unprefixedManifestName
  newTDFChunkedWriter->>newChunkedWriter: set chunked writer configuration
  newChunkedWriter->>segmentWriter.Finalize: pass zipstream manifest-name option
  segmentWriter.Finalize->>segmentWriter.Finalize: select manifest entry name
Loading

Suggested reviewers: elizabethhealy

Merge Risk: ⚪ Minimal · up to 4ec7a

Archives keep the existing 0.manifest.json entry name by default, so older readers are unaffected. The spec-named manifest.json is available as an explicit option, and the reader accepts both names. The PR title and description still say the writer emits manifest.json by default and should be updated to match the opt-in behavior. No merge-blocking risk was found.

### Architecture Summary

Architecture risk: 🔵 Low · up to 094d6

The change affects 1 system.

Changed systems: sdk

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — sdk (service) was modified; 6 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in sdk/tdf_segment_defaults_test.go: rewriteManifest replaced direct named-file archive access with TDFReader operations that retrieve the emitted manifest and full payload independently of the manifest entry name; manifest JSON unmarshalling now converts the returned manifest value to bytes.
  • observed — Modified behavior in sdk/internal/zipstream/zip_headers.go: TDFManifestFileName now uses manifest.json; the former 0.manifest.json value is exposed as TDFManifestFileNameOffspec, while TDFPayloadFileName remains 0.payload.
  • observed — Modified behavior in sdk/tdf_test.go: Test_ValidateSchema "emptymanifest" changer now checks f.Name == zipstream.TDFManifestFileName instead of the literal "0.manifest.json" when deciding whether to overwrite the manifest with {}.
  • observed — Modified behavior in sdk/tdf_test.go: Test_ValidateSchema "nojsonchange" changer now references zipstream.TDFManifestFileName when skipping non-manifest entries, replacing the literal "0.manifest.json" comparison.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 72.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 18 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: adding an opt-in option for an unprefixed manifest entry name.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the names in the stream,
Two manifest labels now gleam.
The old one stays by default,
The new one joins when called out.
ZIP leaves rustle; tests confirm the theme.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 183.454763ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 123.881886ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 431.297513ms
Throughput 231.86 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 51.738456778s
Average Latency 516.1248ms
Throughput 96.64 requests/second

@pflynn-virtru pflynn-virtru changed the title fix(sdk): accept spec-named manifest.json when reading a ZTDF (phase 1 of #3513) fix(sdk): accept spec-named manifest.json when reading a ZTDF Sep 15, 2026
@pflynn-virtru
pflynn-virtru marked this pull request as draft September 15, 2026 15:12
@pflynn-virtru pflynn-virtru changed the title fix(sdk): accept spec-named manifest.json when reading a ZTDF fix(sdk): accept spec-named manifest.json Sep 15, 2026
@pflynn-virtru
pflynn-virtru force-pushed the fix/3513-manifest-json-reader-tolerance branch from cd0e8a2 to e35ee47 Compare September 15, 2026 15:12
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 242.127473ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 118.906912ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 421.879806ms
Throughput 237.03 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 59.168800454s
Average Latency 590.476379ms
Throughput 84.50 requests/second

@pflynn-virtru
pflynn-virtru force-pushed the fix/3513-manifest-json-reader-tolerance branch from e35ee47 to acb8f8d Compare September 15, 2026 15:37
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 246.287195ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 135.820461ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 421.487234ms
Throughput 237.26 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 58.922109879s
Average Latency 587.71189ms
Throughput 84.86 requests/second

@pflynn-virtru
pflynn-virtru force-pushed the fix/3513-manifest-json-reader-tolerance branch from acb8f8d to 997a398 Compare September 15, 2026 15:56
@pflynn-virtru pflynn-virtru changed the title fix(sdk): accept spec-named manifest.json fix(sdk)!: name the ZTDF manifest entry manifest.json per spec Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 236.272644ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 132.542806ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 424.86126ms
Throughput 235.37 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 57.587188682s
Average Latency 574.700716ms
Throughput 86.82 requests/second

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 187.021581ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 102.468047ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 362.416167ms
Throughput 275.93 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 46.729315611s
Average Latency 466.062425ms
Throughput 107.00 requests/second

@pflynn-virtru
pflynn-virtru force-pushed the fix/3513-manifest-json-reader-tolerance branch from ad2e466 to c497bbd Compare September 15, 2026 17:51
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 244.018796ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 139.382536ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 448.911605ms
Throughput 222.76 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 59.600231555s
Average Latency 594.66338ms
Throughput 83.89 requests/second

@pflynn-virtru
pflynn-virtru force-pushed the fix/3513-manifest-json-reader-tolerance branch from c497bbd to 051b4c0 Compare September 15, 2026 17:58
@github-actions

Copy link
Copy Markdown
Contributor

@pflynn-virtru
pflynn-virtru marked this pull request as ready for review September 15, 2026 18:01
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 240.42446ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 127.392391ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 457.75458ms
Throughput 218.46 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 1m0.16267852s
Average Latency 600.255396ms
Throughput 83.11 requests/second

@pflynn-virtru
pflynn-virtru added this pull request to stack #4052 September 15, 2026 18:18
pflynn-virtru added a commit to opentdf/web-sdk that referenced this pull request Sep 15, 2026
The OpenTDF spec puts the manifest at the archive root under `manifest.json`.
This SDK writes and reads `0.manifest.json`. The `0.` prefix is a holdover from
an early design that anticipated several payload/manifest pairs per archive;
that design never shipped.

Write: `writeStream` emits `manifest.json`.

Read: `loadTDFStream` and `getPolicyId` resolve the entry through
`manifestEntryName()`, which prefers the spec name and falls back to the legacy
name, so archives written by earlier releases keep opening.

The dual-name handling lives at the TDF layer rather than in `ZipReader`, whose
signature is unchanged — its other lookup is a payload path that must stay
exact. Resolution runs against the central directory, not against a failed read,
so an oversized manifest under the spec name stays a size error instead of
silently yielding the superseded legacy entry.

`0.payload` is unchanged: it is recorded in the manifest's `payload.url`, so
renaming it would alter manifest contents rather than just archive layout.

BREAKING CHANGE: archives written by this SDK name the manifest entry
`manifest.json`. Readers that look it up by exact name — web-sdk <= 0.20,
java-sdk, client-cpp — cannot read them until they accept the spec name.

Refs opentdf/platform#3513, opentdf/platform#4049, opentdf/tests#600.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
pflynn-virtru added a commit to opentdf/web-sdk that referenced this pull request Sep 15, 2026
The OpenTDF spec puts the manifest at the archive root under `manifest.json`.
This SDK writes and reads `0.manifest.json`. The `0.` prefix is a holdover from
an early design that anticipated several payload/manifest pairs per archive;
that design never shipped.

Write: `writeStream` emits `manifest.json`.

Read: `loadTDFStream` and `getPolicyId` resolve the entry through
`manifestEntryName()`, which prefers the spec name and falls back to the
off-spec name, so archives written by earlier releases keep opening.

The dual-name handling lives at the TDF layer rather than in `ZipReader`, whose
signature is unchanged — its other lookup is a payload path that must stay
exact. Resolution runs against the central directory, not against a failed read,
so an oversized manifest under the spec name stays a size error instead of
silently yielding the superseded off-spec entry.

`0.payload` is unchanged: it is recorded in the manifest's `payload.url`, so
renaming it would alter manifest contents rather than just archive layout.

BREAKING CHANGE: archives written by this SDK name the manifest entry
`manifest.json`. Readers that look it up by exact name — web-sdk <= 0.20,
java-sdk, client-cpp — cannot read them until they accept the spec name.

Refs opentdf/platform#3513, opentdf/platform#4049, opentdf/tests#600.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
pflynn-virtru added a commit to opentdf/java-sdk that referenced this pull request Sep 15, 2026
The OpenTDF spec puts the manifest at the archive root under
`manifest.json`. This SDK wrote and read `0.manifest.json`, an exact
lookup with no fallback, so a TDF produced by any implementation written
against the published spec was rejected outright.

Write: `TDFWriter` emits `manifest.json`.

Read: `TDFReader` accepts either name, preferring `manifest.json` when an
archive carries both, so archives from earlier releases keep working.

`SDK.isTDF` accepts either name too, and no longer requires the archive
to hold exactly two entries -- the spec fixes where the manifest lives,
not what else the archive may hold, and the count check made isTDF
stricter than the reader it screens for.

`0.payload` is unchanged: it is recorded in the manifest's payload URL
field, so renaming it would alter manifest contents rather than just
archive layout.

Refs opentdf/platform#3513
Follows opentdf/platform#4049

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
@pflynn-virtru pflynn-virtru changed the title fix(sdk)!: name the ZTDF manifest entry manifest.json per spec fix(sdk)!: name the TDF manifest entry manifest.json per spec Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 246.682158ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 138.414091ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 420.457613ms
Throughput 237.84 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 1m0.420310726s
Average Latency 602.773055ms
Throughput 82.75 requests/second

pflynn-virtru added a commit to opentdf/java-sdk that referenced this pull request Oct 1, 2026
Refs opentdf/platform#3513. The read half of #405, split out so it can
land on its own.

## Problem

The [OpenTDF spec](https://opentdf.io/spec#tdf-structure) names the
manifest entry in a `.tdf` archive `manifest.json`:

> The `manifest.json` file MUST be in JSON format and reside within the
root of the OpenTDF Zip archive.

This SDK writes and reads `0.manifest.json`. On the read side
`TDFReader` does an exact `containsKey` with no fallback, so a TDF
produced by any implementation written against the published spec is
rejected with `tdf doesn't contain a manifest` before any schema check
runs. `SDK.isTDF` carries its own copy of the literal, so such an
archive is screened out one step earlier still.

The `0.` prefix is a holdover from an early design that anticipated
several payload/manifest pairs per archive. That design never shipped.

## Change

**Read** — `TDFReader` prefers `manifest.json` and falls back to
`0.manifest.json`. Preference matters rather than first-match: an
archive carrying both must not have its conformant entry passed over for
the superseded one.

**Sniffing** — `SDK.isTDF` accepts either name, so it does not reject an
archive the reader can now read. It also no longer requires the archive
to hold *exactly* two entries. That check was scoped out at first as
unrelated to the entry name, but review pointed out it is not: an
archive carrying both manifest names holds three entries, and
`prefersTheSpecNameWhenAnArchiveCarriesBoth` is precisely the case the
reader now handles — so the sniffer would reject what the reader it
screens for accepts. The count was never part of the structure `isTDF`
describes; the spec fixes where the manifest lives, not what else the
archive may hold.

**Duplicate entry names** — a zip may legally list one name twice, and
`Collectors.toMap` turned that into an `IllegalStateException` out of
`TDFReader`'s constructor: outside its declared `throws SDKException,
IOException`, outside what a caller screening untrusted input catches,
and outside the fuzz targets' catch list. Dropping the count check above
made it newly reachable through `isTDF`, so it is rejected here as an
`IllegalArgumentException` alongside the constructor's other
malformed-input paths.

An archive carrying *both* manifest names is still read, not rejected.
Those are two distinct entries and the spec settles which one wins; a
single name listed twice offers no principled choice. Worth being
explicit about the tradeoff, since the root signature covers only
`integrityInformation.segments` — not `policy`, not `keyAccess[].url` —
and no hash covers the entry name: a DEK holder can file two
internally-valid manifests over one ciphertext and have this SDK enforce
one while a `0.`-keyed peer enforces the other. That ambiguity predates
this PR; preferring the spec name relocates which side Java lands on
rather than creating it. Failing closed would be a cross-SDK decision,
not a Java-only one.

**Write is untouched.** `TDFWriter` still emits `0.manifest.json`, so
this release produces byte-identical archives and every existing reader
keeps working. The new `TDF_MANIFEST_FILE_NAME_SPEC` constant is
read-side only. Flipping the writer is the breaking half and stays in
#405.

`0.payload` is untouched. It is recorded in the manifest's
`payload.url`, so renaming it would alter manifest contents rather than
just archive layout, and neither the spec nor opentdf/platform#4049
fixes it.

## Testing

Ported from #405, minus the write-side test:

| Test | Failure without the change |
|---|---|
| `TDFReaderTest.readsManifestUnderTheSpecName` |
`IllegalArgumentException: tdf doesn't contain a manifest` |
| `TDFReaderTest.readsThePayloadAlongsideASpecNamedManifest` | same |
| `TDFReaderTest.prefersTheSpecNameWhenAnArchiveCarriesBoth` | returned
the non-aligned manifest |
| `SDKTest.testExaminingTDFWithSpecManifestName` | `false` |
| `SDKTest.testExaminingTDFWithBothManifestNames` | `false` -- three
entries |
| `SDKTest.testExaminingTDFWithAnExtraEntry` | `false` -- three entries
|
| `TDFReaderTest.rejectsAnArchiveThatListsOneNameTwice` |
`IllegalStateException: Duplicate key 0.payload` |
| `TDFReaderTest.rejectsAnArchiveThatListsTheManifestNameTwice` | same,
for `manifest.json` |

`prefersTheSpecNameWhenAnArchiveCarriesBoth` files a *different*
manifest under each name, so it cannot pass by reading whichever entry
the reader happened to pick.

Guarding behavior that must not change:
`TDFReaderTest.readsManifestUnderTheOffspecName`,
`TDFReaderTest.rejectsAnArchiveWithNoManifestUnderEitherName`,
`SDKTest.testExaminingZipWithNoManifest`,
`SDKTest.testExaminingZipWithNoPayload`, and the pre-existing
`SDKTest.testExaminingValidZTDF` / `testExaminingManifest`, which run
against the checked-in non-aligned-named `sample.txt.tdf` fixture. The
two negative `isTDF` cases hold two entries each, so they failed for the
name they were missing rather than for their entry count even before the
count check came out.

Review of this branch added four tests that pin rules nothing else held
down: `rejectsNearMissManifestEntryNames` (a basename or
case-insensitive lookup passed the suite before, so `sub/manifest.json`
would have been read as the manifest despite the spec's root-only rule),
`SDKTest.testExaminingLargerZipWithNoManifest` (both negatives held two
entries, so "any zip over two entries is a TDF" passed), and the two
duplicate-name cases above.

**Verification:** Local Java execution was unavailable because this
machine has no JDK or Maven. GitHub CI passed `mavenverify`, Platform
Integration (including FIPS), and the full cross-SDK xtest matrix on the
PR head; the previously failing matrix job passed on rerun. The new
tests were also watched failing against unmodified production code on
#405's branch, where the shared code is identical.

## Downstream impact

None. Readers gain a name they accept and `isTDF` gains archive shapes
it recognizes; nothing either previously accepted is taken away, and no
archive this SDK writes changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Compatibility**
* Improved support for TDF archives using the standard `manifest.json`
entry name.
* Continued support for archives using the SDK’s existing
`0.manifest.json` entry name.
* When both manifest names are present, the standard `manifest.json`
entry is used.
* Archives may include additional entries beyond the manifest and
payload.

* **Bug Fixes**
* TDF detection now correctly recognizes valid archives and rejects
those missing a manifest or payload.
* Archives containing duplicate entry names are now rejected with a
clear error.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
Co-authored-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Add WithUnprefixedManifestName option that writes the manifest under the
zip entry name manifest.json (the name the TDF spec defines) instead of
the legacy 0.manifest.json. Pre-4.3.0 WithTargetMode overrides this
option and keeps the legacy name.

Readers that predate manifest.json support, such as Go SDK releases
before v0.35.0, cannot open TDFs created with this option.

Restore deleted assertion in testGetManifestBeforeAndAfterFinalize
that verifies GetManifest returns a clone. Update godoc for chunked
and experimental options to reference compatibility caveats. Fix
gocritic appendAssign lint issue by using bytes.Join instead of append.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
@pflynn-virtru
pflynn-virtru force-pushed the fix/3513-manifest-json-reader-tolerance branch from 094d60e to 4ec7aa7 Compare October 19, 2026 22:58
@pflynn-virtru pflynn-virtru changed the title fix(sdk)!: name the TDF manifest entry manifest.json per spec feat(sdk): add opt-in unprefixed manifest entry name Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • otdfctl
  • sdk
  • service
  • lib/fixtures
  • tests-bdd

See the workflow run for details.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:sdk A software development kit, including library, for client applications and inter-service communicati size/s

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant