Skip to content

fix(cli): drop the encrypt-side stdin spool [backport to release/otdfctl/v0.38] - #4108

Merged
elizabethhealy merged 1 commit into
release/otdfctl/v0.38from
backport-3948-to-release/otdfctl/v0.38
Sep 28, 2026
Merged

elizabethhealy merged 1 commit into
release/otdfctl/v0.38from
backport-3948-to-release/otdfctl/v0.38

Conversation

@opentdf-automation

Copy link
Copy Markdown
Contributor

Description

Backport of #3948 to release/otdfctl/v0.38.

@opentdf-automation
opentdf-automation Bot requested a review from a team as a code owner September 28, 2026 14:38
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 7c226032-8d68-421c-aaba-ccb463f6ef59

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

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

@opentdf-automation
opentdf-automation Bot force-pushed the backport-3948-to-release/otdfctl/v0.38 branch from 0cddf72 to 355b822 Compare September 28, 2026 14:40
@github-actions

Copy link
Copy Markdown
Contributor

@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 221.672335ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 451.414856ms
Throughput 221.53 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 58.200679846s
Average Latency 580.21333ms
Throughput 85.91 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 252.523912ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 427.421257ms
Throughput 233.96 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 1m2.390199909s
Average Latency 622.519787ms
Throughput 80.14 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 218.682137ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 453.263143ms
Throughput 220.62 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 1m0.291764126s
Average Latency 601.360845ms
Throughput 82.93 requests/second

> **Part 19 of 20** in the DSPX-2604 re-cut. Base branch:
`dspx-2604-base-19`.
>
> This stack replaces #3782 / #3865 / #3921, which stay open and
untouched
> until it lands. Nothing here is a rebase of those branches — the work
was
> re-cut from the ticket so each PR stands on its own.

### Proposed Changes

DSPX-4499 fixed encrypt's OOM by streaming, but CreateTDF still required
an
io.ReadSeeker, so piped stdin had to be spooled to a temporary file
first. That
traded the whole-payload allocation for a disk write and a writable
TMPDIR --
better, but not the point. Now that CreateTDF takes an io.Reader, the
pipe goes
straight to the SDK.

A file is still opened seekably, on purpose. The SDK measures a seekable
payload and keeps the archive in the compact ZIP32 layout; an
unmeasurable one
has to be ZIP64, because the choice is baked into the payload's local
file
header before the first segment goes out. So `encrypt file.txt` is
unchanged
byte for byte, and `... | encrypt` produces a slightly larger TDF than
it did
when it was spooled. That is the trade, and it is the right way round:
nobody
should need a writable temp directory to encrypt a stream.

MIME sniffing is what made this more than a deletion. It reads the first
megabyte and previously seeked back to zero, which a pipe cannot do.
Wrapping
the input in a bufio.Reader is not an option either -- that hides the
Seeker
and would silently flip every file encrypt to ZIP64. detectMimeType now
returns
a reader alongside the type: the same reader for a seekable input,
rewound; the
sniffed prefix pushed back with io.MultiReader for anything else. A
megabyte in
memory at most, and only when --mime-type was not given.

Testing: TestDetectMimeTypePreservesThePayload now runs each case twice,
once
over a reader whose Seek method is hidden, and asserts the payload
arrives
whole either way. TestDetectMimeTypeKeepsSeekability guards the ZIP32
layout
directly, since nothing else would fail if a file came back wrapped. e2e
gains
"encrypt measures a file and streams a pipe", which reads the local file
header's extra field length to tell the two layouts apart -- both forms
round-trip, so a quiet return to spooling would show up nowhere else.

### Checklist

- [x] I have added or updated unit tests
- [x] I have added or updated integration tests (if appropriate)
- [x] I have added or updated documentation

### Testing Instructions

```
cd otdfctl && go test ./... -race
```

e2e, against a running platform:

```
cd otdfctl && bats e2e/streaming.bats
```

The file is tagged `payload_streaming` and runs in its own pass ahead of
the
parallel batch — see #3939 for why that ordering is load-bearing (the
platform
base key `key-base.bats` sets cannot be un-set, and breaks every
unattributed
encrypt scheduled after it).

The case to look at is `encrypt measures a file and streams a pipe`,
which
asserts the ZIP layout rather than the round trip: a file must stay
ZIP32
(extra field length 0 at offset 28 of the payload's local file header)
and a
pipe must be ZIP64 (non-zero). A quiet return to spooling would show up
nowhere
else, since both forms round-trip fine.

<details>
<summary><b>The full DSPX-2604 stack — 20 PRs</b></summary>

| # | PR | Based on |
|---|----|----------|
| 01 | #3930 chore: bump go.work toolchain to go1.25.12 and simplify an
rt_test condition | `main` |
| 02 | #3931 feat(sdk): make the zipstream clock injectable for
deterministic ZIP output | `main` |
| 03 | #3932 fix(sdk): reject a zipstream write set that omits segment 0
| #3931 |
| 04 | #3933 fix(sdk): map ReadAt plaintext offsets from cumulative
segment sizes | `main` |
| 05 | #3934 chore(sdk): extract integrityAlgorithmString,
createPolicyBinding, signAssertions | `main` |
| 06 | #3935 chore(sdk): add direct tests for createKeyAccess,
encryptMetadata and tdfSalt | `main` |
| 07 | #3936 fix(sdk): fill each segment with io.ReadFull and size the
buffer to the input | `main` |
| 08 | #3937 chore(cli): move streaming IO helpers into pkg | `main` |
| 09 | #3938 fix(cli): stream encrypt instead of buffering the whole
payload | #3937 |
| 10 | #3939 fix(cli): stream decrypt and inspect instead of buffering |
#3938 |
| 11 | #3940 feat(sdk): add a chunked segment writer (experimental) |
`dspx-2604-base-11` = #3932 + #3934 + #3935 |
| 12 | #3941 fix(sdk): stop GetManifest from splitting the key under the
lock | #3940 |
| 13 | #3942 fix(sdk): reject a chunked split naming a KAS with no
resolved public key | #3941 |
| 14 | #3943 chore(sdk): alias experimental/tdf manifest and assertion
types | #3942 |
| 15 | #3944 fix(sdk): emit spec-compliant key access in
experimental/tdf and delegate Writer | #3943 |
| 16 | #3945 feat(sdk): accept io.Reader in CreateTDF and drop the 64 GB
payload cap | #3936 |
| 17 | #3946 chore(sdk): rewrite CreateTDF on top of the chunked writer
| `dspx-2604-base-17` = #3944 + #3945 |
| 18 | #3947 chore(sdk): drop dead TDFConfig fields and deprecate the
TDFFormat enum | #3946 |
| 19 | #3948 fix(cli): drop the encrypt-side stdin spool |
`dspx-2604-base-19` = #3947 + #3939 |
| 20 | #3949 feat(sdk): graduate the chunked writer to stable API |
#3948 |

**Reviewable in parallel right now**, since they sit directly on `main`
and depend on
nothing else: 01, 02, 04, 05, 06, 07, 08.

**Why three PRs have a `dspx-2604-base-*` base.** A GitHub PR takes one
base branch,
but 11, 17 and 19 each build on more than one parent. The `base-*`
branches are empty
merge commits that exist only to join those parents so the PR diff shows
exactly its
own change and nothing else. They contain no code, have no PR of their
own, and go
away once their parents land — retarget the child onto `main` at that
point.

**Wants a cross-SDK xtest run before merge:** 15, 17 (and therefore 20).
They touch
the KAS wire format.

**Red checks you may see are network flakes, not this stack.** Four
distinct ones hit
this batch and all clear on re-run: `golangci-lint config verify` timing
out on
`https://golangci-lint.run/.../golangci.v2.8.jsonschema.json` (fails the
whole `go
(<module>)` job and fail-fast cancels its siblings), the bats installer
getting a 403,
Docker Hub timing out on `keycloak/keycloak:26.4`, and `buf` reporting
"the server
hosted at that remote is unavailable" while the Java SDK generates
sources. The
`govulncheck` step also emits `##[error]` annotations against the
go1.25.11 stdlib, but
it is `continue-on-error: true` and never fails a job — 01 bumps the
toolchain and
clears those annotations.

</details>

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

* **New Features**
* Encryption now supports piped and other non-seekable input, including
larger streams.
* Inputs that cannot be measured in advance produce ZIP64 output;
seekable files continue to use ZIP32 when applicable.
* Regular-file redirects can be read directly without being copied to a
temporary file.
  * Decryption now supports TDFs supplied through pipes and redirects.
* Encrypted output remains compatible with the existing decryption flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
(cherry picked from commit 5603c2e)
@elizabethhealy
elizabethhealy force-pushed the backport-3948-to-release/otdfctl/v0.38 branch from 4628a68 to 165e72c Compare September 28, 2026 18:11
@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 257.499416ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 440.119169ms
Throughput 227.21 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 1m3.048316645s
Average Latency 628.952373ms
Throughput 79.30 requests/second

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • otdfctl
  • service
  • tests-bdd

See the workflow run for details.

@elizabethhealy
elizabethhealy merged commit 12f8ed6 into release/otdfctl/v0.38 Sep 28, 2026
45 of 46 checks passed
@elizabethhealy
elizabethhealy deleted the backport-3948-to-release/otdfctl/v0.38 branch September 28, 2026 18:46
elizabethhealy pushed a commit that referenced this pull request Sep 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.38.1](otdfctl/v0.38.0...otdfctl/v0.38.1)
(2026-09-28)


### Bug Fixes

* **cli:** drop the encrypt-side stdin spool [backport to
release/otdfctl/v0.38]
([#4108](#4108))
([12f8ed6](12f8ed6))
* **deps:** bump SDK from v0.32.0 to v0.33.0 in /otdfctl [backport to
release/otdfctl/v0.38]
([#4111](#4111))
([8d66131](8d66131))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: opentdf-automation[bot] <149537512+opentdf-automation[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant