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 }