1package zipartifacts
2
3import (
4	"compress/gzip"
5	"encoding/binary"
6	"encoding/json"
7	"io"
8	"path"
9	"sort"
10	"strconv"
11
12	zip "gitlab.com/gitlab-org/golang-archive-zip"
13)
14
15type metadata struct {
16	Modified int64  `json:"modified,omitempty"`
17	Mode     string `json:"mode,omitempty"`
18	CRC      uint32 `json:"crc,omitempty"`
19	Size     uint64 `json:"size,omitempty"`
20	Zipped   uint64 `json:"zipped,omitempty"`
21	Comment  string `json:"comment,omitempty"`
22}
23
24const MetadataHeaderPrefix = "\x00\x00\x00&" // length of string below, encoded properly
25const MetadataHeader = "GitLab Build Artifacts Metadata 0.0.2\n"
26
27func newMetadata(file *zip.File) metadata {
28	if file == nil {
29		return metadata{}
30	}
31
32	return metadata{
33		//lint:ignore SA1019 Remove this once the minimum supported version is go 1.10 (go 1.9 and down do not support an alternative)
34		Modified: file.ModTime().Unix(),
35		Mode:     strconv.FormatUint(uint64(file.Mode().Perm()), 8),
36		CRC:      file.CRC32,
37		Size:     file.UncompressedSize64,
38		Zipped:   file.CompressedSize64,
39		Comment:  file.Comment,
40	}
41}
42
43func (m metadata) writeEncoded(output io.Writer) error {
44	j, err := json.Marshal(m)
45	if err != nil {
46		return err
47	}
48	j = append(j, byte('\n'))
49	return writeBytes(output, j)
50}
51
52func writeZipEntryMetadata(output io.Writer, path string, entry *zip.File) error {
53	if err := writeString(output, path); err != nil {
54		return err
55	}
56
57	if err := newMetadata(entry).writeEncoded(output); err != nil {
58		return err
59	}
60
61	return nil
62}
63
64func GenerateZipMetadata(w io.Writer, archive *zip.Reader) error {
65	output := gzip.NewWriter(w)
66	defer output.Close()
67
68	if err := writeString(output, MetadataHeader); err != nil {
69		return err
70	}
71
72	// Write empty error header that we may need in the future
73	if err := writeString(output, "{}"); err != nil {
74		return err
75	}
76
77	// Create map of files in zip archive
78	zipMap := make(map[string]*zip.File, len(archive.File))
79
80	// Add missing entries
81	for _, entry := range archive.File {
82		zipMap[entry.Name] = entry
83
84		for d := path.Dir(entry.Name); d != "." && d != "/"; d = path.Dir(d) {
85			entryDir := d + "/"
86			if _, ok := zipMap[entryDir]; !ok {
87				zipMap[entryDir] = nil
88			}
89		}
90	}
91
92	// Sort paths
93	sortedPaths := make([]string, 0, len(zipMap))
94	for path := range zipMap {
95		sortedPaths = append(sortedPaths, path)
96	}
97	sort.Strings(sortedPaths)
98
99	// Write all files
100	for _, path := range sortedPaths {
101		if err := writeZipEntryMetadata(output, path, zipMap[path]); err != nil {
102			return err
103		}
104	}
105	return nil
106}
107
108func writeBytes(output io.Writer, data []byte) error {
109	err := binary.Write(output, binary.BigEndian, uint32(len(data)))
110	if err == nil {
111		_, err = output.Write(data)
112	}
113	return err
114}
115
116func writeString(output io.Writer, str string) error {
117	return writeBytes(output, []byte(str))
118}
119