diff --git a/storage/sealer/fr32/readers.go b/storage/sealer/fr32/readers.go index 8c5a8ad1242..b499bc422cf 100644 --- a/storage/sealer/fr32/readers.go +++ b/storage/sealer/fr32/readers.go @@ -101,11 +101,37 @@ func (r *unpadReader) readInner(out []byte) (int, error) { r.left -= uint64(todo) n, err := io.ReadAtLeast(r.src, r.work[:todo], int(todo)) - if err != nil && err != io.EOF { - return n, err + if err == io.ErrUnexpectedEOF { + // We got a partial read. This happens when the underlying reader + // doesn't have as much data as expected (e.g., non-power-of-2 pieces). + // Process what we got. + if n > 0 { + // Round down to complete 128-byte chunks + completeChunks := n / 128 + if completeChunks > 0 { + validBytes := completeChunks * 128 + Unpad(r.work[:validBytes], out[:completeChunks*127]) + // Adjust left to reflect that we couldn't read everything + r.left = 0 + return completeChunks * 127, io.EOF + } + } + // Not enough data for even one chunk + return 0, io.EOF + } + if err == io.EOF { + // Clean EOF with no data + if n == 0 { + return 0, io.EOF + } + // Got some data with EOF - shouldn't happen with ReadAtLeast but handle it + return 0, xerrors.Errorf("unexpected EOF with partial read: %d bytes", n) + } + if err != nil { + return 0, err } if n < int(todo) { - return 0, xerrors.Errorf("didn't read enough: %d / %d, left %d, out %d", n, todo, r.left, len(out)) + return 0, xerrors.Errorf("short read without EOF: got %d, expected %d", n, todo) } Unpad(r.work[:todo], out[:todo.Unpadded()]) diff --git a/storage/sealer/fr32/readers_test.go b/storage/sealer/fr32/readers_test.go index 7a61b80f8d2..f6d23782080 100644 --- a/storage/sealer/fr32/readers_test.go +++ b/storage/sealer/fr32/readers_test.go @@ -196,3 +196,50 @@ func TestUnpadReaderLargeReads(t *testing.T) { require.Equal(t, unpadded, result) } + +// TestUnpadReaderPartialPiece tests the edge case where the underlying reader +// has less data than UnpadReader expects. This happens when dealing with pieces +// that are not exact power-of-2 sizes. +// This test captures the fix for the EOF errors that occurred after PR #12884. +func TestUnpadReaderPartialPiece(t *testing.T) { + actualUnpadded := abi.UnpaddedPieceSize(127 * 100) // 100 chunks = 12700 bytes + declaredSize := abi.PaddedPieceSize(128 * 128) // Declare 128 chunks but only have 100 + + // Generate test data + unpadded := make([]byte, actualUnpadded) + n, err := rand.Read(unpadded) + require.NoError(t, err) + require.Equal(t, int(actualUnpadded), n) + + // Pad the data + paddedActual := actualUnpadded.Padded() + padded := make([]byte, paddedActual) + fr32.Pad(unpadded, padded) + + // Create a reader that returns EOF after the actual data + limitedReader := bytes.NewReader(padded) + + // Create UnpadReader with the larger declared size + unpadReader, err := fr32.NewUnpadReader(limitedReader, declaredSize) + require.NoError(t, err) + + // Read all data + result := make([]byte, 0, actualUnpadded*2) + buf := make([]byte, 1024) + + for { + n, err := unpadReader.Read(buf) + if n > 0 { + result = append(result, buf[:n]...) + } + if err == io.EOF { + break + } + // The fix allows UnpadReader to handle partial reads gracefully + require.NoError(t, err, "UnpadReader should handle partial pieces without error") + } + + // Verify we got the correct data + require.Equal(t, int(actualUnpadded), len(result), "Should read all available data") + require.Equal(t, unpadded, result, "Data should match original") +}