github.com/hashicorp/vault/sdk@v0.11.0/helper/compressutil/compress.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package compressutil
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"compress/lzw"
    10  	"fmt"
    11  	"io"
    12  
    13  	"github.com/golang/snappy"
    14  	"github.com/hashicorp/errwrap"
    15  	"github.com/pierrec/lz4"
    16  )
    17  
    18  const (
    19  	// A byte value used as a canary prefix for the compressed information
    20  	// which is used to distinguish if a JSON input is compressed or not.
    21  	// The value of this constant should not be a first character of any
    22  	// valid JSON string.
    23  
    24  	CompressionTypeGzip        = "gzip"
    25  	CompressionCanaryGzip byte = 'G'
    26  
    27  	CompressionTypeLZW        = "lzw"
    28  	CompressionCanaryLZW byte = 'L'
    29  
    30  	CompressionTypeSnappy        = "snappy"
    31  	CompressionCanarySnappy byte = 'S'
    32  
    33  	CompressionTypeLZ4        = "lz4"
    34  	CompressionCanaryLZ4 byte = '4'
    35  )
    36  
    37  // SnappyReadCloser embeds the snappy reader which implements the io.Reader
    38  // interface. The decompress procedure in this utility expects an
    39  // io.ReadCloser. This type implements the io.Closer interface to retain the
    40  // generic way of decompression.
    41  type CompressUtilReadCloser struct {
    42  	io.Reader
    43  }
    44  
    45  // Close is a noop method implemented only to satisfy the io.Closer interface
    46  func (c *CompressUtilReadCloser) Close() error {
    47  	return nil
    48  }
    49  
    50  // CompressionConfig is used to select a compression type to be performed by
    51  // Compress and Decompress utilities.
    52  // Supported types are:
    53  // * CompressionTypeLZW
    54  // * CompressionTypeGzip
    55  // * CompressionTypeSnappy
    56  // * CompressionTypeLZ4
    57  //
    58  // When using CompressionTypeGzip, the compression levels can also be chosen:
    59  // * gzip.DefaultCompression
    60  // * gzip.BestSpeed
    61  // * gzip.BestCompression
    62  type CompressionConfig struct {
    63  	// Type of the compression algorithm to be used
    64  	Type string
    65  
    66  	// When using Gzip format, the compression level to employ
    67  	GzipCompressionLevel int
    68  }
    69  
    70  // Compress places the canary byte in a buffer and uses the same buffer to fill
    71  // in the compressed information of the given input. The configuration supports
    72  // two type of compression: LZW and Gzip. When using Gzip compression format,
    73  // if GzipCompressionLevel is not specified, the 'gzip.DefaultCompression' will
    74  // be assumed.
    75  func Compress(data []byte, config *CompressionConfig) ([]byte, error) {
    76  	var buf bytes.Buffer
    77  	var writer io.WriteCloser
    78  	var err error
    79  
    80  	if config == nil {
    81  		return nil, fmt.Errorf("config is nil")
    82  	}
    83  
    84  	// Write the canary into the buffer and create writer to compress the
    85  	// input data based on the configured type
    86  	switch config.Type {
    87  	case CompressionTypeLZW:
    88  		buf.Write([]byte{CompressionCanaryLZW})
    89  		writer = lzw.NewWriter(&buf, lzw.LSB, 8)
    90  
    91  	case CompressionTypeGzip:
    92  		buf.Write([]byte{CompressionCanaryGzip})
    93  
    94  		switch {
    95  		case config.GzipCompressionLevel == gzip.BestCompression,
    96  			config.GzipCompressionLevel == gzip.BestSpeed,
    97  			config.GzipCompressionLevel == gzip.DefaultCompression:
    98  			// These are valid compression levels
    99  		default:
   100  			// If compression level is set to NoCompression or to
   101  			// any invalid value, fallback to Defaultcompression
   102  			config.GzipCompressionLevel = gzip.DefaultCompression
   103  		}
   104  		writer, err = gzip.NewWriterLevel(&buf, config.GzipCompressionLevel)
   105  
   106  	case CompressionTypeSnappy:
   107  		buf.Write([]byte{CompressionCanarySnappy})
   108  		writer = snappy.NewBufferedWriter(&buf)
   109  
   110  	case CompressionTypeLZ4:
   111  		buf.Write([]byte{CompressionCanaryLZ4})
   112  		writer = lz4.NewWriter(&buf)
   113  
   114  	default:
   115  		return nil, fmt.Errorf("unsupported compression type")
   116  	}
   117  
   118  	if err != nil {
   119  		return nil, errwrap.Wrapf("failed to create a compression writer: {{err}}", err)
   120  	}
   121  
   122  	if writer == nil {
   123  		return nil, fmt.Errorf("failed to create a compression writer")
   124  	}
   125  
   126  	// Compress the input and place it in the same buffer containing the
   127  	// canary byte.
   128  	if _, err = writer.Write(data); err != nil {
   129  		return nil, errwrap.Wrapf("failed to compress input data: err: {{err}}", err)
   130  	}
   131  
   132  	// Close the io.WriteCloser
   133  	if err = writer.Close(); err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	// Return the compressed bytes with canary byte at the start
   138  	return buf.Bytes(), nil
   139  }
   140  
   141  // Decompress checks if the first byte in the input matches the canary byte.
   142  // If the first byte is a canary byte, then the input past the canary byte
   143  // will be decompressed using the method specified in the given configuration.
   144  // If the first byte isn't a canary byte, then the utility returns a boolean
   145  // value indicating that the input was not compressed.
   146  func Decompress(data []byte) ([]byte, bool, error) {
   147  	bytes, _, notCompressed, err := DecompressWithCanary(data)
   148  	return bytes, notCompressed, err
   149  }
   150  
   151  // DecompressWithCanary checks if the first byte in the input matches the canary byte.
   152  // If the first byte is a canary byte, then the input past the canary byte
   153  // will be decompressed using the method specified in the given configuration. The type of compression used is also
   154  // returned. If the first byte isn't a canary byte, then the utility returns a boolean
   155  // value indicating that the input was not compressed.
   156  func DecompressWithCanary(data []byte) ([]byte, string, bool, error) {
   157  	var err error
   158  	var reader io.ReadCloser
   159  	var compressionType string
   160  	if data == nil || len(data) == 0 {
   161  		return nil, "", false, fmt.Errorf("'data' being decompressed is empty")
   162  	}
   163  
   164  	canary := data[0]
   165  	cData := data[1:]
   166  
   167  	switch canary {
   168  	// If the first byte matches the canary byte, remove the canary
   169  	// byte and try to decompress the data that is after the canary.
   170  	case CompressionCanaryGzip:
   171  		if len(data) < 2 {
   172  			return nil, "", false, fmt.Errorf("invalid 'data' after the canary")
   173  		}
   174  		reader, err = gzip.NewReader(bytes.NewReader(cData))
   175  		compressionType = CompressionTypeGzip
   176  
   177  	case CompressionCanaryLZW:
   178  		if len(data) < 2 {
   179  			return nil, "", false, fmt.Errorf("invalid 'data' after the canary")
   180  		}
   181  		reader = lzw.NewReader(bytes.NewReader(cData), lzw.LSB, 8)
   182  		compressionType = CompressionTypeLZW
   183  
   184  	case CompressionCanarySnappy:
   185  		if len(data) < 2 {
   186  			return nil, "", false, fmt.Errorf("invalid 'data' after the canary")
   187  		}
   188  		reader = &CompressUtilReadCloser{
   189  			Reader: snappy.NewReader(bytes.NewReader(cData)),
   190  		}
   191  		compressionType = CompressionTypeSnappy
   192  
   193  	case CompressionCanaryLZ4:
   194  		if len(data) < 2 {
   195  			return nil, "", false, fmt.Errorf("invalid 'data' after the canary")
   196  		}
   197  		reader = &CompressUtilReadCloser{
   198  			Reader: lz4.NewReader(bytes.NewReader(cData)),
   199  		}
   200  		compressionType = CompressionTypeLZ4
   201  
   202  	default:
   203  		// If the first byte doesn't match the canary byte, it means
   204  		// that the content was not compressed at all. Indicate the
   205  		// caller that the input was not compressed.
   206  		return nil, "", true, nil
   207  	}
   208  	if err != nil {
   209  		return nil, "", false, errwrap.Wrapf("failed to create a compression reader: {{err}}", err)
   210  	}
   211  	if reader == nil {
   212  		return nil, "", false, fmt.Errorf("failed to create a compression reader")
   213  	}
   214  
   215  	// Close the io.ReadCloser
   216  	defer reader.Close()
   217  
   218  	// Read all the compressed data into a buffer
   219  	var buf bytes.Buffer
   220  	if _, err = io.Copy(&buf, reader); err != nil {
   221  		return nil, "", false, err
   222  	}
   223  
   224  	return buf.Bytes(), compressionType, false, nil
   225  }