github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/fsutil/targzfs/builders.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // 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, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package targzfs 19 20 import ( 21 "archive/tar" 22 "bytes" 23 "compress/gzip" 24 "errors" 25 "fmt" 26 "io" 27 "io/fs" 28 "path" 29 "path/filepath" 30 31 "github.com/zntrio/harp/v2/pkg/sdk/ioutil" 32 ) 33 34 // FromFile creates an archive filesystem from a filename. 35 func FromFile(root fs.FS, name string) (fs.FS, error) { 36 // Open the target file 37 fn, err := root.Open(filepath.Clean(name)) 38 if err != nil { 39 return nil, fmt.Errorf("unable to open archive %q: %w", name, err) 40 } 41 42 // Delegate to reader constructor. 43 return FromReader(fn) 44 } 45 46 // FromReader exposes the contents of the given reader (which is a .tar.gz file) 47 // as an fs.FS. 48 func FromReader(r io.Reader) (fs.FS, error) { 49 gz, err := gzip.NewReader(r) 50 if err != nil { 51 return nil, fmt.Errorf("unable to open .tar.gz file: %w", err) 52 } 53 54 // Retrieve TAR content from GZIP 55 var ( 56 tarContents bytes.Buffer 57 ) 58 59 // Chunked read with hard limit to prevent/reduce zipbomb vulnerability 60 // exploitation. 61 if err := ioutil.Copy(maxDecompressedSize, &tarContents, gz); err != nil { 62 return nil, fmt.Errorf("unable to decompress the archive: %w", err) 63 } 64 65 // Close the gzip decompressor 66 if err := gz.Close(); err != nil { 67 return nil, fmt.Errorf("unable to close gzip reader: %w", err) 68 } 69 70 // TAR format reader 71 tarReader := tar.NewReader(&tarContents) 72 73 // Prepare in-memory filesystem. 74 ret := &tarGzFs{ 75 files: make(map[string]*tarEntry), 76 rootEntries: make([]fs.DirEntry, 0, 10), 77 rootEntry: nil, 78 } 79 80 for { 81 // Iterate on each file entry 82 hdr, err := tarReader.Next() 83 if err != nil { 84 if errors.Is(err, io.EOF) { 85 break 86 } 87 return nil, fmt.Errorf("unable to read .tar.gz entry: %w", err) 88 } 89 if hdr != nil && len(ret.files) > maxFileCount { 90 return nil, errors.New("interrupted extraction, too many files in the archive") 91 } 92 93 // Clean file path. (ZipSlip) 94 name := path.Clean(hdr.Name) 95 if name == "." { 96 continue 97 } 98 99 // Load content in memory 100 var ( 101 fileContents bytes.Buffer 102 ) 103 104 // Chunked read with hard limit to prevent/reduce post decompression 105 // explosion 106 if err := ioutil.Copy(maxFileSize, &fileContents, tarReader); err != nil { 107 return nil, fmt.Errorf("unable to copy file content to memory: %w", err) 108 } 109 110 // Register file 111 e := &tarEntry{ 112 h: hdr, 113 b: fileContents.Bytes(), 114 entries: nil, 115 } 116 117 // Add as file entry 118 ret.files[name] = e 119 120 // Create directories 121 dir := path.Dir(name) 122 if dir == "." { 123 ret.rootEntries = append(ret.rootEntries, e) 124 } else { 125 if parent, ok := ret.files[dir]; ok { 126 parent.entries = append(parent.entries, e) 127 } 128 } 129 } 130 131 // No error 132 return ret, nil 133 }