github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/vault/sdk/helper/compressutil/compress.go (about)

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