github.com/apache/arrow/go/v14@v14.0.1/parquet/file/file_reader.go (about)

     1  // Licensed to the Apache Software Foundation (ASF) under one
     2  // or more contributor license agreements.  See the NOTICE file
     3  // distributed with this work for additional information
     4  // regarding copyright ownership.  The ASF licenses this file
     5  // to you under the Apache License, Version 2.0 (the
     6  // "License"); you may not use this file except in compliance
     7  // with the License.  You may obtain a copy of the License at
     8  //
     9  // http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing, software
    12  // distributed under the License is distributed on an "AS IS" BASIS,
    13  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  // See the License for the specific language governing permissions and
    15  // limitations under the License.
    16  
    17  package file
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/binary"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"runtime"
    26  	"sync"
    27  
    28  	"github.com/apache/arrow/go/v14/arrow/memory"
    29  	"github.com/apache/arrow/go/v14/parquet"
    30  	"github.com/apache/arrow/go/v14/parquet/internal/encryption"
    31  	"github.com/apache/arrow/go/v14/parquet/metadata"
    32  	"golang.org/x/xerrors"
    33  )
    34  
    35  const (
    36  	footerSize uint32 = 8
    37  )
    38  
    39  var (
    40  	magicBytes                  = []byte("PAR1")
    41  	magicEBytes                 = []byte("PARE")
    42  	errInconsistentFileMetadata = xerrors.New("parquet: file is smaller than indicated metadata size")
    43  )
    44  
    45  // Reader is the main interface for reading a parquet file
    46  type Reader struct {
    47  	r             parquet.ReaderAtSeeker
    48  	props         *parquet.ReaderProperties
    49  	metadata      *metadata.FileMetaData
    50  	footerOffset  int64
    51  	fileDecryptor encryption.FileDecryptor
    52  
    53  	bufferPool sync.Pool
    54  }
    55  
    56  type ReadOption func(*Reader)
    57  
    58  // WithReadProps specifies a specific reader properties instance to use, rather
    59  // than using the default ReaderProperties.
    60  func WithReadProps(props *parquet.ReaderProperties) ReadOption {
    61  	return func(r *Reader) {
    62  		r.props = props
    63  	}
    64  }
    65  
    66  // WithMetadata allows providing a specific FileMetaData object rather than reading
    67  // the file metadata from the file itself.
    68  func WithMetadata(m *metadata.FileMetaData) ReadOption {
    69  	return func(r *Reader) {
    70  		r.metadata = m
    71  	}
    72  }
    73  
    74  // OpenParquetFile will return a Reader for the given parquet file on the local file system.
    75  //
    76  // Optionally the file can be memory mapped for faster reading. If no read properties are provided
    77  // then the default ReaderProperties will be used. The WithMetadata option can be used to provide
    78  // a FileMetaData object rather than reading the file metadata from the file.
    79  func OpenParquetFile(filename string, memoryMap bool, opts ...ReadOption) (*Reader, error) {
    80  	var source parquet.ReaderAtSeeker
    81  
    82  	var err error
    83  	if memoryMap {
    84  		source, err = mmapOpen(filename)
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  	} else {
    89  		source, err = os.Open(filename)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  	}
    94  	return NewParquetReader(source, opts...)
    95  }
    96  
    97  // NewParquetReader returns a FileReader instance that reads a parquet file which can be read from r.
    98  // This reader needs to support Read, ReadAt and Seeking.
    99  //
   100  // If no read properties are provided then the default ReaderProperties will be used. The WithMetadata
   101  // option can be used to provide a FileMetaData object rather than reading the file metadata from the file.
   102  func NewParquetReader(r parquet.ReaderAtSeeker, opts ...ReadOption) (*Reader, error) {
   103  	var err error
   104  	f := &Reader{r: r}
   105  	for _, o := range opts {
   106  		o(f)
   107  	}
   108  
   109  	if f.footerOffset <= 0 {
   110  		f.footerOffset, err = r.Seek(0, io.SeekEnd)
   111  		if err != nil {
   112  			return nil, fmt.Errorf("parquet: could not retrieve footer offset: %w", err)
   113  		}
   114  	}
   115  
   116  	if f.props == nil {
   117  		f.props = parquet.NewReaderProperties(memory.NewGoAllocator())
   118  	}
   119  
   120  	f.bufferPool = sync.Pool{
   121  		New: func() interface{} {
   122  			buf := memory.NewResizableBuffer(f.props.Allocator())
   123  			runtime.SetFinalizer(buf, func(obj *memory.Buffer) {
   124  				obj.Release()
   125  			})
   126  			return buf
   127  		},
   128  	}
   129  
   130  	if f.metadata == nil {
   131  		return f, f.parseMetaData()
   132  	}
   133  
   134  	return f, nil
   135  }
   136  
   137  // BufferPool returns the internal buffer pool being utilized by this reader.
   138  // This is primarily for use by the pqarrow.FileReader or anything that builds
   139  // on top of the Reader and constructs their own ColumnReaders (like the
   140  // RecordReader)
   141  func (f *Reader) BufferPool() *sync.Pool {
   142  	return &f.bufferPool
   143  }
   144  
   145  // Close will close the current reader, and if the underlying reader being used
   146  // is an `io.Closer` then Close will be called on it too.
   147  func (f *Reader) Close() error {
   148  	if r, ok := f.r.(io.Closer); ok {
   149  		return r.Close()
   150  	}
   151  	return nil
   152  }
   153  
   154  // MetaData returns the underlying FileMetadata object
   155  func (f *Reader) MetaData() *metadata.FileMetaData { return f.metadata }
   156  
   157  // parseMetaData handles parsing the metadata from the opened file.
   158  func (f *Reader) parseMetaData() error {
   159  	if f.footerOffset <= int64(footerSize) {
   160  		return fmt.Errorf("parquet: file too small (size=%d)", f.footerOffset)
   161  	}
   162  
   163  	buf := make([]byte, footerSize)
   164  	// backup 8 bytes to read the footer size (first four bytes) and the magic bytes (last 4 bytes)
   165  	n, err := f.r.ReadAt(buf, f.footerOffset-int64(footerSize))
   166  	if err != nil && err != io.EOF {
   167  		return fmt.Errorf("parquet: could not read footer: %w", err)
   168  	}
   169  	if n != len(buf) {
   170  		return fmt.Errorf("parquet: could not read %d bytes from end of file", len(buf))
   171  	}
   172  
   173  	size := int64(binary.LittleEndian.Uint32(buf[:4]))
   174  	if size < 0 || size+int64(footerSize) > f.footerOffset {
   175  		return errInconsistentFileMetadata
   176  	}
   177  
   178  	fileDecryptProps := f.props.FileDecryptProps
   179  
   180  	switch {
   181  	case bytes.Equal(buf[4:], magicBytes): // non-encrypted metadata
   182  		buf = make([]byte, size)
   183  		if _, err := f.r.ReadAt(buf, f.footerOffset-int64(footerSize)-size); err != nil {
   184  			return fmt.Errorf("parquet: could not read footer: %w", err)
   185  		}
   186  
   187  		f.metadata, err = metadata.NewFileMetaData(buf, nil)
   188  		if err != nil {
   189  			return fmt.Errorf("parquet: could not read footer: %w", err)
   190  		}
   191  
   192  		if !f.metadata.IsSetEncryptionAlgorithm() {
   193  			if fileDecryptProps != nil && !fileDecryptProps.PlaintextFilesAllowed() {
   194  				return fmt.Errorf("parquet: applying decryption properties on plaintext file")
   195  			}
   196  		} else {
   197  			if err := f.parseMetaDataEncryptedFilePlaintextFooter(fileDecryptProps, buf); err != nil {
   198  				return err
   199  			}
   200  		}
   201  	case bytes.Equal(buf[4:], magicEBytes): // encrypted metadata
   202  		buf = make([]byte, size)
   203  		if _, err := f.r.ReadAt(buf, f.footerOffset-int64(footerSize)-size); err != nil {
   204  			return fmt.Errorf("parquet: could not read footer: %w", err)
   205  		}
   206  
   207  		if fileDecryptProps == nil {
   208  			return xerrors.New("could not read encrypted metadata, no decryption found in reader's properties")
   209  		}
   210  
   211  		fileCryptoMetadata, err := metadata.NewFileCryptoMetaData(buf)
   212  		if err != nil {
   213  			return err
   214  		}
   215  		algo := fileCryptoMetadata.EncryptionAlgorithm()
   216  		fileAad, err := f.handleAadPrefix(fileDecryptProps, &algo)
   217  		if err != nil {
   218  			return err
   219  		}
   220  		f.fileDecryptor = encryption.NewFileDecryptor(fileDecryptProps, fileAad, algo.Algo, string(fileCryptoMetadata.KeyMetadata()), f.props.Allocator())
   221  
   222  		f.metadata, err = metadata.NewFileMetaData(buf[fileCryptoMetadata.Len():], f.fileDecryptor)
   223  		if err != nil {
   224  			return fmt.Errorf("parquet: could not read footer: %w", err)
   225  		}
   226  	default:
   227  		return fmt.Errorf("parquet: magic bytes not found in footer. Either the file is corrupted or this isn't a parquet file")
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func (f *Reader) handleAadPrefix(fileDecrypt *parquet.FileDecryptionProperties, algo *parquet.Algorithm) (string, error) {
   234  	aadPrefixInProps := fileDecrypt.AadPrefix()
   235  	aadPrefix := []byte(aadPrefixInProps)
   236  	fileHasAadPrefix := algo.Aad.AadPrefix != nil && len(algo.Aad.AadPrefix) > 0
   237  	aadPrefixInFile := algo.Aad.AadPrefix
   238  
   239  	if algo.Aad.SupplyAadPrefix && aadPrefixInProps == "" {
   240  		return "", xerrors.New("AAD Prefix used for file encryption but not stored in file and not suppliedin decryption props")
   241  	}
   242  
   243  	if fileHasAadPrefix {
   244  		if aadPrefixInProps != "" {
   245  			if aadPrefixInProps != string(aadPrefixInFile) {
   246  				return "", xerrors.New("AAD prefix in file and in properties but not the same")
   247  			}
   248  		}
   249  		aadPrefix = aadPrefixInFile
   250  		if fileDecrypt.Verifier != nil {
   251  			fileDecrypt.Verifier.Verify(string(aadPrefix))
   252  		}
   253  	} else {
   254  		if !algo.Aad.SupplyAadPrefix && aadPrefixInProps != "" {
   255  			return "", xerrors.New("AAD Prefix set in decryptionproperties but was not used for file encryption")
   256  		}
   257  		if fileDecrypt.Verifier != nil {
   258  			return "", xerrors.New("AAD Prefix Verifier is set but AAD Prefix not found in file")
   259  		}
   260  	}
   261  	return string(append(aadPrefix, algo.Aad.AadFileUnique...)), nil
   262  }
   263  
   264  func (f *Reader) parseMetaDataEncryptedFilePlaintextFooter(decryptProps *parquet.FileDecryptionProperties, data []byte) error {
   265  	if decryptProps != nil {
   266  		algo := f.metadata.EncryptionAlgorithm()
   267  		fileAad, err := f.handleAadPrefix(decryptProps, &algo)
   268  		if err != nil {
   269  			return err
   270  		}
   271  		f.fileDecryptor = encryption.NewFileDecryptor(decryptProps, fileAad, algo.Algo, string(f.metadata.GetFooterSigningKeyMetadata()), f.props.Allocator())
   272  		// set the InternalFileDecryptor in the metadata as well, as it's used
   273  		// for signature verification and for ColumnChunkMetaData creation.
   274  		f.metadata.FileDecryptor = f.fileDecryptor
   275  		if decryptProps.PlaintextFooterIntegrity() {
   276  			if len(data)-f.metadata.Size() != encryption.GcmTagLength+encryption.NonceLength {
   277  				return xerrors.New("failed reading metadata for encryption signature")
   278  			}
   279  
   280  			if !f.metadata.VerifySignature(data[f.metadata.Size():]) {
   281  				return xerrors.New("parquet crypto signature verification failed")
   282  			}
   283  		}
   284  	}
   285  	return nil
   286  }
   287  
   288  // WriterVersion returns the Application Version that was written in the file
   289  // metadata
   290  func (f *Reader) WriterVersion() *metadata.AppVersion {
   291  	return f.metadata.WriterVersion()
   292  }
   293  
   294  // NumRows returns the total number of rows in this parquet file.
   295  func (f *Reader) NumRows() int64 {
   296  	return f.metadata.GetNumRows()
   297  }
   298  
   299  // NumRowGroups returns the total number of row groups in this file.
   300  func (f *Reader) NumRowGroups() int {
   301  	return len(f.metadata.GetRowGroups())
   302  }
   303  
   304  // RowGroup returns a reader for the desired (0-based) row group
   305  func (f *Reader) RowGroup(i int) *RowGroupReader {
   306  	rg := f.metadata.RowGroups[i]
   307  
   308  	return &RowGroupReader{
   309  		fileMetadata:  f.metadata,
   310  		rgMetadata:    metadata.NewRowGroupMetaData(rg, f.metadata.Schema, f.WriterVersion(), f.fileDecryptor),
   311  		props:         f.props,
   312  		r:             f.r,
   313  		sourceSz:      f.footerOffset,
   314  		fileDecryptor: f.fileDecryptor,
   315  		bufferPool:    &f.bufferPool,
   316  	}
   317  }