-
Notifications
You must be signed in to change notification settings - Fork 97
/
pack.go
362 lines (324 loc) · 12.5 KB
/
pack.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
package resource
import (
"archive/zip"
"bytes"
"crypto/sha256"
"fmt"
"github.com/muhammadmuzzammil1998/jsonc"
"io"
"os"
"path/filepath"
"strconv"
"strings"
)
// Pack is a container of a resource pack parsed from a directory or a .zip archive (or .mcpack). It holds
// methods that may be used to get information about the resource pack.
type Pack struct {
// manifest is the manifest of the resource pack. It contains information about the pack such as the name,
// version and description.
manifest *Manifest
// content is a bytes.Reader that contains the full content of the zip file. It is used to send the full
// data to a client.
content *bytes.Reader
// contentKey is the key used to encrypt the files. The client uses this to decrypt the resource pack if encrypted.
// If nothing is encrypted, this field can be left as an empty string.
contentKey string
// checksum is the SHA256 checksum of the full content of the file. It is sent to the client so that it
// can 'verify' the download.
checksum [32]byte
}
// Compile compiles a resource pack found at the path passed. The resource pack must either be a zip archive
// (extension does not matter, could be .zip or .mcpack), or a directory containing a resource pack. In the
// case of a directory, the directory is compiled into an archive and the pack is parsed from that.
// Compile operates assuming the resource pack has a 'manifest.json' file in it. If it does not, the function
// will fail and return an error.
func Compile(path string) (*Pack, error) {
return compile(path)
}
// MustCompile compiles a resource pack found at the path passed. The resource pack must either be a zip
// archive (extension does not matter, could be .zip or .mcpack), or a directory containing a resource pack.
// In the case of a directory, the directory is compiled into an archive and the pack is parsed from that.
// Compile operates assuming the resource pack has a 'manifest.json' file in it. If it does not, the function
// will fail and return an error.
// Unlike Compile, MustCompile does not return an error and panics if an error occurs instead.
func MustCompile(path string) *Pack {
pack, err := compile(path)
if err != nil {
panic(err)
}
return pack
}
// FromBytes parses an archived resource pack written to a raw byte slice passed. The data must be a valid
// zip archive and contain a pack manifest in order for the function to succeed.
// FromBytes saves the data to a temporary archive.
func FromBytes(data []byte) (*Pack, error) {
tempFile, err := os.CreateTemp("", "resource_pack_archive-*.mcpack")
if err != nil {
return nil, fmt.Errorf("error creating temp zip archive: %v", err)
}
_, _ = tempFile.Write(data)
if err := tempFile.Close(); err != nil {
return nil, fmt.Errorf("error closing temp zip archive: %v", err)
}
pack, parseErr := Compile(tempFile.Name())
if err := os.Remove(tempFile.Name()); err != nil {
return nil, fmt.Errorf("error removing temp zip archive: %v", err)
}
return pack, parseErr
}
// Name returns the name of the resource pack.
func (pack *Pack) Name() string {
return pack.manifest.Header.Name
}
// UUID returns the UUID of the resource pack.
func (pack *Pack) UUID() string {
return pack.manifest.Header.UUID
}
// Description returns the description of the resource pack.
func (pack *Pack) Description() string {
return pack.manifest.Header.Description
}
// Version returns the string version of the resource pack. It is guaranteed to have 3 digits in it, joined
// by a dot.
func (pack *Pack) Version() string {
return strconv.Itoa(pack.manifest.Header.Version[0]) + "." + strconv.Itoa(pack.manifest.Header.Version[1]) + "." + strconv.Itoa(pack.manifest.Header.Version[2])
}
// Modules returns all modules that the resource pack exists out of. Resource packs usually have only one
// module, but may have more depending on their functionality.
func (pack *Pack) Modules() []Module {
return pack.manifest.Modules
}
// Dependencies returns all dependency resource packs that must be loaded in order for this resource pack to
// function correctly.
func (pack *Pack) Dependencies() []Dependency {
return pack.manifest.Dependencies
}
// HasScripts checks if any of the modules of the resource pack have the type 'client_data', meaning they have
// scripts in them.
func (pack *Pack) HasScripts() bool {
for _, module := range pack.manifest.Modules {
if module.Type == "client_data" {
// The module has the client_data type, meaning it holds client scripts.
return true
}
}
return false
}
// HasBehaviours checks if any of the modules of the resource pack have either the type 'data' or
// 'client_data', meaning they contain behaviours (or scripts).
func (pack *Pack) HasBehaviours() bool {
for _, module := range pack.manifest.Modules {
if module.Type == "client_data" || module.Type == "data" {
// The module has the client_data or data type, meaning it holds behaviours.
return true
}
}
return false
}
// HasTextures checks if any of the modules of the resource pack have the type 'resources', meaning they have
// textures in them.
func (pack *Pack) HasTextures() bool {
for _, module := range pack.manifest.Modules {
if module.Type == "resources" {
// The module has the resources type, meaning it holds textures.
return true
}
}
return false
}
// HasWorldTemplate checks if the resource compiled holds a level.dat in it, indicating that the resource is
// a world template.
func (pack *Pack) HasWorldTemplate() bool {
return pack.manifest.worldTemplate
}
// Checksum returns the SHA256 checksum made from the full, compressed content of the resource pack archive.
// It is transmitted as a string over network.
func (pack *Pack) Checksum() [32]byte {
return pack.checksum
}
// Len returns the total length in bytes of the content of the archive that contained the resource pack.
func (pack *Pack) Len() int {
return pack.content.Len()
}
// DataChunkCount returns the amount of chunks the data of the resource pack is split into if each chunk has
// a specific length.
func (pack *Pack) DataChunkCount(length int) int {
count := pack.Len() / length
if pack.Len()%length != 0 {
count++
}
return count
}
// Encrypted returns if the resource pack has been encrypted with a content key or not.
func (pack *Pack) Encrypted() bool {
return pack.contentKey != ""
}
// ContentKey returns the encryption key used to encrypt the resource pack. If the pack is not encrypted then
// this can be empty.
func (pack *Pack) ContentKey() string {
return pack.contentKey
}
// ReadAt reads len(b) bytes from the resource pack's archive data at offset off and copies it into b. The
// amount of bytes read n is returned.
func (pack *Pack) ReadAt(b []byte, off int64) (n int, err error) {
return pack.content.ReadAt(b, off)
}
// WithContentKey creates a copy of the pack and sets the encryption key to the key provided, after which the
// new Pack is returned.
func (pack Pack) WithContentKey(key string) *Pack {
pack.contentKey = key
return &pack
}
// Manifest returns the manifest found in the manifest.json of the resource pack. It contains information
// about the pack such as its name.
func (pack *Pack) Manifest() Manifest {
return *pack.manifest
}
// String returns a readable representation of the resource pack. It implements the Stringer interface.
func (pack *Pack) String() string {
return fmt.Sprintf("%v v%v (%v): %v", pack.Name(), pack.Version(), pack.UUID(), pack.Description())
}
// compile compiles the resource pack found in path, either a zip archive or a directory, and returns a
// resource pack if successful.
func compile(path string) (*Pack, error) {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("error opening resource pack path: %v", err)
}
if info.IsDir() {
temp, err := createTempArchive(path)
if err != nil {
return nil, err
}
// We set the path to the temp zip archive we just made.
path = temp.Name()
// Make sure we close the temp file and remove it at the end. We don't need to keep it, as we read all
// the content in a byte slice.
_ = temp.Close()
defer func() {
_ = os.Remove(temp.Name())
}()
}
// First we read the manifest to ensure that it exists and is valid.
manifest, err := readManifest(path)
if err != nil {
return nil, fmt.Errorf("error reading manifest: %v", err)
}
// Then we read the entire content of the zip archive into a byte slice and compute the SHA256 checksum
// and a reader.
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading resource pack file content: %v", err)
}
checksum := sha256.Sum256(content)
contentReader := bytes.NewReader(content)
return &Pack{manifest: manifest, checksum: checksum, content: contentReader}, nil
}
// createTempArchive creates a zip archive from the files in the path passed and writes it to a temporary
// file, which is returned when successful.
func createTempArchive(path string) (*os.File, error) {
// We've got a directory which we need to load. Provided we need to send compressed zip data to the
// client, we compile it to a zip archive in a temporary file.
temp, err := os.CreateTemp("", "resource_pack-*.mcpack")
if err != nil {
return nil, fmt.Errorf("error creating temp zip file: %v", err)
}
writer := zip.NewWriter(temp)
if err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(path, filePath)
if err != nil {
return fmt.Errorf("error finding relative path: %v", err)
}
// Make sure to replace backslashes with forward slashes as Go zip only allows that.
relPath = strings.Replace(relPath, `\`, "/", -1)
// Always ignore '.' as it is not a real file/folder.
if relPath == "." {
return nil
}
s, err := os.Stat(filePath)
if err != nil {
return fmt.Errorf("error getting stat of file path %v: %w", filePath, err)
}
if s.IsDir() {
// This is a directory: Go zip requires you add forward slashes at the end to create directories.
_, _ = writer.Create(relPath + "/")
return nil
}
f, err := writer.Create(relPath)
if err != nil {
return fmt.Errorf("error creating new zip file: %v", err)
}
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("error opening resource pack file %v: %v", filePath, err)
}
data, _ := io.ReadAll(file)
// Write the original content into the 'zip file' so that we write compressed data to the file.
if _, err := f.Write(data); err != nil {
return fmt.Errorf("error writing file data to zip: %v", err)
}
_ = file.Close()
return nil
}); err != nil {
return nil, fmt.Errorf("error building zip archive: %v", err)
}
_ = writer.Close()
return temp, nil
}
// packReader wraps around a zip.Reader to provide file finding functionality.
type packReader struct {
*zip.ReadCloser
}
// find attempts to find a file in a zip reader. If found, it returns an Open()ed reader of the file that may
// be used to read data from the file.
func (reader packReader) find(fileName string) (io.ReadCloser, error) {
for _, file := range reader.File {
base := filepath.Base(file.Name)
if file.Name != fileName && base != fileName {
continue
}
fileReader, err := file.Open()
if err != nil {
return nil, fmt.Errorf("error opening zip file %v: %v", file.Name, err)
}
return fileReader, nil
}
return nil, fmt.Errorf("could not find '%v' in zip", fileName)
}
// readManifest reads the manifest from the resource pack located at the path passed. If not found in the root
// of the resource pack, it will also attempt to find it deeper down into the archive.
func readManifest(path string) (*Manifest, error) {
r, err := zip.OpenReader(path)
if err != nil {
return nil, fmt.Errorf("error opening zip reader: %v", err)
}
reader := packReader{ReadCloser: r}
defer func() {
_ = r.Close()
}()
// Try to find the manifest file in the zip.
manifestFile, err := reader.find("manifest.json")
if err != nil {
return nil, fmt.Errorf("error loading manifest: %v", err)
}
defer func() {
_ = manifestFile.Close()
}()
// Read all data from the manifest file so that we can decode it into a Manifest struct.
allData, err := io.ReadAll(manifestFile)
if err != nil {
return nil, fmt.Errorf("error reading from manifest file: %v", err)
}
manifest := &Manifest{}
if err := jsonc.Unmarshal(allData, manifest); err != nil {
return nil, fmt.Errorf("error decoding manifest JSON: %v (data: %v)", err, string(allData))
}
manifest.Header.UUID = strings.ToLower(manifest.Header.UUID)
if _, err := reader.find("level.dat"); err == nil {
manifest.worldTemplate = true
}
return manifest, nil
}