This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new c03882ff fix(parquet): reject short page writes (#1050)
c03882ff is described below

commit c03882ffcdb819866da2496e70f8f86d11d7c577
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 20:03:19 2026 +0200

    fix(parquet): reject short page writes (#1050)
    
    ## Summary
    
    - return `io.ErrShortWrite` when a direct dictionary or data page body
    write accepts fewer bytes than requested
    - avoid updating page counters and indexes after an incomplete body
    write
    - cover both direct page paths with a short-writing sink
    
    ## Testing
    
    - `go test ./parquet/file`
---
 parquet/file/page_writer.go               |  12 ++--
 parquet/file/page_writer_internal_test.go | 109 ++++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+), 6 deletions(-)

diff --git a/parquet/file/page_writer.go b/parquet/file/page_writer.go
index 7ab2b024..144d39b7 100644
--- a/parquet/file/page_writer.go
+++ b/parquet/file/page_writer.go
@@ -320,9 +320,6 @@ func (pw *serializedPageWriter) WriteDictionaryPage(page 
*DictionaryPage) (int64
        pageHdr.DataPageHeaderV2 = nil
 
        startPos := pw.sink.Tell()
-       if pw.dictPageOffset == 0 {
-               pw.dictPageOffset = int64(startPos)
-       }
 
        if pw.metaEncryptor != nil {
                if err := pw.updateEncryption(encryption.DictPageHeaderModule); 
err != nil {
@@ -339,6 +336,9 @@ func (pw *serializedPageWriter) WriteDictionaryPage(page 
*DictionaryPage) (int64
        }
 
        written += headerSize
+       if pw.dictPageOffset == 0 {
+               pw.dictPageOffset = int64(startPos)
+       }
 
        pw.totalUncompressed += int64(uncompressed + headerSize)
        pw.totalCompressed = int64(written)
@@ -383,9 +383,6 @@ func (pw *serializedPageWriter) WriteDataPage(page 
DataPage) (int64, error) {
        }
 
        startPos := pw.sink.Tell()
-       if pw.pageOrdinal == 0 {
-               pw.dataPageOffset = int64(startPos)
-       }
 
        if pw.metaEncryptor != nil {
                if err := pw.updateEncryption(encryption.DataPageHeaderModule); 
err != nil {
@@ -401,6 +398,9 @@ func (pw *serializedPageWriter) WriteDataPage(page 
DataPage) (int64, error) {
                return int64(written), err
        }
        written += headerSize
+       if pw.pageOrdinal == 0 {
+               pw.dataPageOffset = int64(startPos)
+       }
 
        // collect page index
        if pw.columnIndexBuilder != nil {
diff --git a/parquet/file/page_writer_internal_test.go 
b/parquet/file/page_writer_internal_test.go
new file mode 100644
index 00000000..38c5a236
--- /dev/null
+++ b/parquet/file/page_writer_internal_test.go
@@ -0,0 +1,109 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package file
+
+import (
+       "bytes"
+       "errors"
+       "io"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/compress"
+       "github.com/stretchr/testify/require"
+)
+
+type shortPageSink struct {
+       data        []byte
+       pos         int64
+       headerShort bool
+}
+
+func (s *shortPageSink) Tell() int64 { return s.pos }
+
+func (s *shortPageSink) Write(p []byte) (int, error) {
+       n := len(p)
+       if s.headerShort || bytes.Equal(p, s.data) {
+               n--
+       }
+       s.pos += int64(n)
+       if n != len(p) {
+               return n, io.ErrShortWrite
+       }
+       return n, nil
+}
+
+func TestSerializedPageWriterRejectsShortHeaderWrites(t *testing.T) {
+       sink := &shortPageSink{headerShort: true, pos: 17}
+       writer, err := NewPageWriter(sink, compress.Codecs.Uncompressed,
+               compress.DefaultCompressionLevel, nil, -1, -1, 
memory.DefaultAllocator, false, nil, nil)
+       require.NoError(t, err)
+
+       buf := memory.NewBufferBytes([]byte("page body"))
+       defer buf.Release()
+       _, err = writer.WriteDictionaryPage(NewDictionaryPage(buf, 1, 
parquet.Encodings.Plain))
+       require.ErrorIs(t, err, io.ErrShortWrite)
+       serialized := writer.(*serializedPageWriter)
+       require.Zero(t, serialized.NumValues())
+       require.Zero(t, serialized.DictionaryPageOffset())
+}
+
+func TestSerializedPageWriterRejectsShortWrites(t *testing.T) {
+       data := []byte("page body")
+
+       tests := []struct {
+               name        string
+               wantWritten int64
+               write       func(PageWriter, *memory.Buffer) (int64, error)
+       }{
+               {
+                       name:        "dictionary page",
+                       wantWritten: 0,
+                       write: func(writer PageWriter, data *memory.Buffer) 
(int64, error) {
+                               return 
writer.WriteDictionaryPage(NewDictionaryPage(data, 1, parquet.Encodings.Plain))
+                       },
+               },
+               {
+                       name:        "data page",
+                       wantWritten: int64(len(data) - 1),
+                       write: func(writer PageWriter, data *memory.Buffer) 
(int64, error) {
+                               return writer.WriteDataPage(NewDataPageV1(data, 
1, parquet.Encodings.Plain,
+                                       parquet.Encodings.RLE, 
parquet.Encodings.RLE, int32(data.Len())))
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       sink := &shortPageSink{data: data, pos: 17}
+                       writer, err := NewPageWriter(sink, 
compress.Codecs.Uncompressed,
+                               compress.DefaultCompressionLevel, nil, -1, -1, 
memory.DefaultAllocator, false, nil, nil)
+                       require.NoError(t, err)
+
+                       buf := memory.NewBufferBytes(data)
+                       defer buf.Release()
+                       written, err := tt.write(writer, buf)
+                       require.True(t, errors.Is(err, io.ErrShortWrite))
+                       require.Equal(t, tt.wantWritten, written)
+                       serialized := writer.(*serializedPageWriter)
+                       require.Zero(t, serialized.NumValues())
+                       require.Zero(t, serialized.DictionaryPageOffset())
+                       require.Zero(t, serialized.DataPageoffset())
+               })
+       }
+}

Reply via email to