1package archiver
2
3import (
4	"archive/tar"
5	"bytes"
6	"fmt"
7	"io"
8	"os"
9	"path/filepath"
10	"strconv"
11	"strings"
12)
13
14// Tar is for Tar format
15var Tar tarFormat
16
17func init() {
18	RegisterFormat("Tar", Tar)
19}
20
21type tarFormat struct{}
22
23func (tarFormat) Match(filename string) bool {
24	return strings.HasSuffix(strings.ToLower(filename), ".tar") || isTar(filename)
25}
26
27const tarBlockSize int = 512
28
29// isTar checks the file has the Tar format header by reading its beginning
30// block.
31func isTar(tarPath string) bool {
32	f, err := os.Open(tarPath)
33	if err != nil {
34		return false
35	}
36	defer f.Close()
37
38	buf := make([]byte, tarBlockSize)
39	if _, err = io.ReadFull(f, buf); err != nil {
40		return false
41	}
42
43	return hasTarHeader(buf)
44}
45
46// hasTarHeader checks passed bytes has a valid tar header or not. buf must
47// contain at least 512 bytes and if not, it always returns false.
48func hasTarHeader(buf []byte) bool {
49	if len(buf) < tarBlockSize {
50		return false
51	}
52
53	b := buf[148:156]
54	b = bytes.Trim(b, " \x00") // clean up all spaces and null bytes
55	if len(b) == 0 {
56		return false // unknown format
57	}
58	hdrSum, err := strconv.ParseUint(string(b), 8, 64)
59	if err != nil {
60		return false
61	}
62
63	// According to the go official archive/tar, Sun tar uses signed byte
64	// values so this calcs both signed and unsigned
65	var usum uint64
66	var sum int64
67	for i, c := range buf {
68		if 148 <= i && i < 156 {
69			c = ' ' // checksum field itself is counted as branks
70		}
71		usum += uint64(uint8(c))
72		sum += int64(int8(c))
73	}
74
75	if hdrSum != usum && int64(hdrSum) != sum {
76		return false // invalid checksum
77	}
78
79	return true
80}
81
82// Write outputs a .tar file to a Writer containing the
83// contents of files listed in filePaths. File paths can
84// be those of regular files or directories. Regular
85// files are stored at the 'root' of the archive, and
86// directories are recursively added.
87func (tarFormat) Write(output io.Writer, filePaths []string) error {
88	return writeTar(filePaths, output, "")
89}
90
91// Make creates a .tar file at tarPath containing the
92// contents of files listed in filePaths. File paths can
93// be those of regular files or directories. Regular
94// files are stored at the 'root' of the archive, and
95// directories are recursively added.
96func (tarFormat) Make(tarPath string, filePaths []string) error {
97	out, err := os.Create(tarPath)
98	if err != nil {
99		return fmt.Errorf("error creating %s: %v", tarPath, err)
100	}
101	defer out.Close()
102
103	return writeTar(filePaths, out, tarPath)
104}
105
106func writeTar(filePaths []string, output io.Writer, dest string) error {
107	tarWriter := tar.NewWriter(output)
108	defer tarWriter.Close()
109
110	return tarball(filePaths, tarWriter, dest)
111}
112
113// tarball writes all files listed in filePaths into tarWriter, which is
114// writing into a file located at dest.
115func tarball(filePaths []string, tarWriter *tar.Writer, dest string) error {
116	for _, fpath := range filePaths {
117		err := tarFile(tarWriter, fpath, dest)
118		if err != nil {
119			return err
120		}
121	}
122	return nil
123}
124
125// tarFile writes the file at source into tarWriter. It does so
126// recursively for directories.
127func tarFile(tarWriter *tar.Writer, source, dest string) error {
128	sourceInfo, err := os.Stat(source)
129	if err != nil {
130		return fmt.Errorf("%s: stat: %v", source, err)
131	}
132
133	var baseDir string
134	if sourceInfo.IsDir() {
135		baseDir = filepath.Base(source)
136	}
137
138	return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
139		if err != nil {
140			return fmt.Errorf("error walking to %s: %v", path, err)
141		}
142
143		header, err := tar.FileInfoHeader(info, path)
144		if err != nil {
145			return fmt.Errorf("%s: making header: %v", path, err)
146		}
147
148		if baseDir != "" {
149			header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
150		}
151
152		if header.Name == dest {
153			// our new tar file is inside the directory being archived; skip it
154			return nil
155		}
156
157		if info.IsDir() {
158			header.Name += "/"
159		}
160
161		err = tarWriter.WriteHeader(header)
162		if err != nil {
163			return fmt.Errorf("%s: writing header: %v", path, err)
164		}
165
166		if info.IsDir() {
167			return nil
168		}
169
170		if header.Typeflag == tar.TypeReg {
171			file, err := os.Open(path)
172			if err != nil {
173				return fmt.Errorf("%s: open: %v", path, err)
174			}
175			defer file.Close()
176
177			_, err = io.CopyN(tarWriter, file, info.Size())
178			if err != nil && err != io.EOF {
179				return fmt.Errorf("%s: copying contents: %v", path, err)
180			}
181		}
182		return nil
183	})
184}
185
186// Read untars a .tar file read from a Reader and puts
187// the contents into destination.
188func (tarFormat) Read(input io.Reader, destination string) error {
189	return untar(tar.NewReader(input), destination)
190}
191
192// Open untars source and puts the contents into destination.
193func (tarFormat) Open(source, destination string) error {
194	f, err := os.Open(source)
195	if err != nil {
196		return fmt.Errorf("%s: failed to open archive: %v", source, err)
197	}
198	defer f.Close()
199
200	return Tar.Read(f, destination)
201}
202
203// untar un-tarballs the contents of tr into destination.
204func untar(tr *tar.Reader, destination string) error {
205	for {
206		header, err := tr.Next()
207		if err == io.EOF {
208			break
209		} else if err != nil {
210			return err
211		}
212
213		if err := untarFile(tr, header, destination); err != nil {
214			return err
215		}
216	}
217	return nil
218}
219
220// untarFile untars a single file from tr with header header into destination.
221func untarFile(tr *tar.Reader, header *tar.Header, destination string) error {
222	err := sanitizeExtractPath(header.Name, destination)
223	if err != nil {
224		return err
225	}
226
227	destpath := filepath.Join(destination, header.Name)
228
229	switch header.Typeflag {
230	case tar.TypeDir:
231		return mkdir(destpath)
232	case tar.TypeReg, tar.TypeRegA, tar.TypeChar, tar.TypeBlock, tar.TypeFifo:
233		return writeNewFile(destpath, tr, header.FileInfo().Mode())
234	case tar.TypeSymlink:
235		return writeNewSymbolicLink(destpath, header.Linkname)
236	case tar.TypeLink:
237		return writeNewHardLink(destpath, filepath.Join(destination, header.Linkname))
238	case tar.TypeXGlobalHeader:
239		// ignore the pax global header from git generated tarballs
240		return nil
241	default:
242		return fmt.Errorf("%s: unknown type flag: %c", header.Name, header.Typeflag)
243	}
244}
245