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 }