From b86c632b165956ee6e837e49950cdbdb500bb3a5 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Thu, 15 Oct 2026 17:12:14 -0400 Subject: [PATCH] fix(sdk): skip the segment ending at the cursor in Reader.WriteTo WriteTo compared with < when skipping segments before the cursor, so a start offset exactly on a segment boundary read, verified and decrypted the preceding segment only to write zero bytes. Over an HTTP range reader that is a wasted segment fetch per partial decrypt. ReadAt already uses <=. Part of DSPX-4886. Signed-off-by: sujan kota --- sdk/tdf.go | 4 +++- sdk/tdf_readat_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index 5d2003f585..a0d67b2e62 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1033,7 +1033,9 @@ func (r *Reader) WriteTo(writer io.Writer) (int64, error) { return totalBytes, err } - if decryptedDataOffset+segSize < r.cursor { + // <= also skips the segment ending exactly at the cursor; it would + // otherwise be read and decrypted only to write zero bytes. + if decryptedDataOffset+segSize <= r.cursor { decryptedDataOffset += segSize payloadReadOffset += encryptedSegSize continue diff --git a/sdk/tdf_readat_test.go b/sdk/tdf_readat_test.go index 7d6585fd8a..a95c785504 100644 --- a/sdk/tdf_readat_test.go +++ b/sdk/tdf_readat_test.go @@ -440,6 +440,50 @@ func (s *TDFSuite) Test_TDFReaderReadAtBoundaries() { } } +// countingReadSeeker counts bytes read through it. +type countingReadSeeker struct { + io.ReadSeeker + n int64 +} + +func (c *countingReadSeeker) Read(p []byte) (int, error) { + n, err := c.ReadSeeker.Read(p) + c.n += int64(n) + return n, err +} + +// Test_TDFReaderWriteToSkipsSegmentBeforeBoundary checks that WriteTo from an +// offset exactly on a segment boundary reads none of the segment ending there. +// Over an HTTP range reader that segment would be a wasted fetch and decrypt. +func (s *TDFSuite) Test_TDFReaderWriteToSkipsSegmentBeforeBoundary() { + plain := readAtSweepPayload() + var tdfBuf bytes.Buffer + _, err := s.sdk.CreateTDF(&tdfBuf, bytes.NewReader(plain), + WithKasInformation(KASInfo{URL: s.kasTestURLLookup["http://localhost:65432/"]}), + WithSegmentSize(readAtSegSize), + ) + s.Require().NoError(err) + + src := &countingReadSeeker{ReadSeeker: bytes.NewReader(tdfBuf.Bytes())} + reader, err := s.sdk.LoadTDF(src) + s.Require().NoError(err) + s.Require().Len(reader.manifest.Segments, readAtSegCount) + + _, err = reader.Seek(readAtSegSize, io.SeekStart) + s.Require().NoError(err) + src.n = 0 + var out bytes.Buffer + _, err = reader.WriteTo(&out) + s.Require().NoError(err) + s.Require().Equal(string(plain[readAtSegSize:]), out.String()) + + var want int64 + for _, seg := range reader.manifest.Segments[1:] { + want += seg.EncryptedSize + } + s.Equal(want, src.n, "WriteTo read payload bytes outside the requested range") +} + // Test_TDFReaderMultiSegmentSeekReadWriteTo checks that Read, which routes // through ReadAt, and WriteTo agree on a multi-segment payload. WriteTo walks // cumulative segment sizes on its own, so a disagreement means one of the two