Skip to content

Commit 441152f

Browse files
committed
feat: Add archive extraction support for http(s)
This enables archive extraction for the zip format and the gz/tar/tar.gz formats. If a user provides an http(s) link to one of these formats, pullman will now automatically extract the contents into the destination directory. Signed-off-by: Paul Van Eck <[email protected]>
1 parent 9b49d40 commit 441152f

File tree

3 files changed

+426
-1
lines changed

3 files changed

+426
-1
lines changed

pullman/helpers.go

+193
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,61 @@
1414
package pullman
1515

1616
import (
17+
"archive/tar"
18+
"archive/zip"
19+
"bufio"
20+
"bytes"
21+
"compress/gzip"
1722
"fmt"
1823
"hash/fnv"
24+
"io"
1925
"os"
2026
"path/filepath"
27+
"strings"
2128
)
2229

30+
type FileFormat struct {
31+
MagicBytes []byte
32+
Offset int
33+
Extension string
34+
}
35+
36+
// Magic byte values pulled from: https://en.wikipedia.org/wiki/List_of_file_signatures
37+
var fileFormats = []FileFormat{
38+
{
39+
MagicBytes: []byte{0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30},
40+
Offset: 257,
41+
Extension: "tar",
42+
},
43+
{
44+
MagicBytes: []byte{0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00},
45+
Offset: 257,
46+
Extension: "tar",
47+
},
48+
{
49+
MagicBytes: []byte{0x1F, 0x8B},
50+
Offset: 0,
51+
Extension: "gz",
52+
},
53+
{
54+
MagicBytes: []byte{0x50, 0x4B, 0x03, 0x04},
55+
Offset: 0,
56+
Extension: "zip",
57+
},
58+
59+
{
60+
MagicBytes: []byte{0x50, 0x4B, 0x05, 0x06},
61+
Offset: 0,
62+
Extension: "zip",
63+
},
64+
65+
{
66+
MagicBytes: []byte{0x50, 0x4B, 0x07, 0x08},
67+
Offset: 0,
68+
Extension: "zip",
69+
},
70+
}
71+
2372
// OpenFile will check the path and the filesystem for mismatch errors
2473
func OpenFile(path string) (*os.File, error) {
2574
// resource paths need to be compatible with a local filesystem download
@@ -57,3 +106,147 @@ func HashStrings(strings ...string) string {
57106

58107
return fmt.Sprintf("%#x", h.Sum64())
59108
}
109+
110+
// Extract a zip file into the provided destination directory.
111+
func ExtractZip(filePath string, dest string) error {
112+
zipReader, err := zip.OpenReader(filePath)
113+
if err != nil {
114+
return fmt.Errorf("unable to open '%s' for reading: %w", filePath, err)
115+
}
116+
defer zipReader.Close()
117+
118+
prefix := filepath.Clean(dest) + string(os.PathSeparator)
119+
for _, zipFileEntry := range zipReader.File {
120+
destFilePath := filepath.Join(dest, zipFileEntry.Name)
121+
122+
// Zip slip vulnerability check
123+
if !strings.HasPrefix(destFilePath, prefix) {
124+
return fmt.Errorf("%s: illegal file path", destFilePath)
125+
}
126+
127+
if zipFileEntry.FileInfo().IsDir() {
128+
err = os.MkdirAll(destFilePath, 0755)
129+
if err != nil {
130+
return fmt.Errorf("error creating new directory %s", destFilePath)
131+
}
132+
continue
133+
}
134+
135+
file, fileErr := OpenFile(destFilePath)
136+
if fileErr != nil {
137+
return fmt.Errorf("unable to open local file '%s' for writing: %w", destFilePath, fileErr)
138+
}
139+
defer file.Close()
140+
141+
zippedRc, err := zipFileEntry.Open()
142+
if err != nil {
143+
return fmt.Errorf("error opening zip file entry: %w", err)
144+
}
145+
defer zippedRc.Close()
146+
147+
if _, err = io.Copy(file, zippedRc); err != nil {
148+
return fmt.Errorf("error writing zip resource to local file '%s': %w", destFilePath, err)
149+
}
150+
151+
}
152+
return nil
153+
}
154+
155+
// Extract a tar archive file into the provided destination directory.
156+
func ExtractTar(filePath string, dest string) error {
157+
tarFile, err := os.Open(filePath)
158+
if err != nil {
159+
return fmt.Errorf("unable to open '%s' for reading: %w", filePath, err)
160+
}
161+
defer tarFile.Close()
162+
163+
tr := tar.NewReader(tarFile)
164+
for {
165+
header, err := tr.Next()
166+
167+
if err == io.EOF {
168+
break
169+
}
170+
171+
if err != nil {
172+
return fmt.Errorf("error reading tar archive entry: %w", err)
173+
}
174+
175+
if header == nil {
176+
continue
177+
}
178+
179+
destFilePath := filepath.Join(dest, header.Name)
180+
if header.Typeflag == tar.TypeDir {
181+
err = os.MkdirAll(destFilePath, 0755)
182+
if err != nil {
183+
return fmt.Errorf("error creating new directory %s", destFilePath)
184+
}
185+
continue
186+
}
187+
188+
file, fileErr := OpenFile(destFilePath)
189+
if fileErr != nil {
190+
return fmt.Errorf("unable to open local file '%s' for writing: %w", destFilePath, fileErr)
191+
}
192+
defer file.Close()
193+
if _, err = io.Copy(file, tr); err != nil {
194+
return fmt.Errorf("error writing tar resource to local file '%s': %w", destFilePath, err)
195+
}
196+
}
197+
return nil
198+
}
199+
200+
// Extract a gzip compressed file into the provided destination file path.
201+
func ExtractGzip(filePath string, dest string) error {
202+
gzipFile, err := os.Open(filePath)
203+
if err != nil {
204+
return fmt.Errorf("unable to open '%s' for reading: %w", filePath, err)
205+
}
206+
defer gzipFile.Close()
207+
gzr, err := gzip.NewReader(gzipFile)
208+
if err != nil {
209+
return fmt.Errorf("unable to create gzip reader: %w", err)
210+
}
211+
defer gzr.Close()
212+
213+
file, fileErr := OpenFile(dest)
214+
if fileErr != nil {
215+
return fmt.Errorf("unable to open local file '%s' for writing: %w", dest, fileErr)
216+
}
217+
defer file.Close()
218+
219+
if _, err = io.Copy(file, gzr); err != nil {
220+
return fmt.Errorf("error writing gzip resource to local file '%s': %w", dest, err)
221+
}
222+
223+
return nil
224+
}
225+
226+
// Get the file type based on the first few hundred bytes of the stream.
227+
// If the file isn't one of the expected formats, nil is returned.
228+
// If an error occurs while determining the file format, nil is returned.
229+
func GetFileFormat(filePath string) *FileFormat {
230+
231+
file, err := os.Open(filePath)
232+
if err != nil {
233+
return nil
234+
}
235+
defer file.Close()
236+
237+
r := bufio.NewReader(file)
238+
239+
// Due to the tar magic bytes offset, this is the minimum number of bytes we need to read.
240+
numBytes := 264
241+
fileBytes, err := r.Peek(numBytes)
242+
if err != nil {
243+
return nil
244+
}
245+
246+
for _, format := range fileFormats {
247+
if bytes.Equal(fileBytes[format.Offset:format.Offset+len(format.MagicBytes)], format.MagicBytes) {
248+
return &format
249+
}
250+
}
251+
return nil
252+
}

0 commit comments

Comments
 (0)