-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpack.go
More file actions
192 lines (173 loc) · 4.88 KB
/
Copy pathpack.go
File metadata and controls
192 lines (173 loc) · 4.88 KB
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
package main
import (
"bytes"
"encoding/json"
"fmt"
"maps"
"os"
"path/filepath"
"strings"
"sync"
"unicode"
"github.com/iteplenky/bedrock-pack-tools/v3/internal/lang"
"github.com/iteplenky/gophertunnel/minecraft/protocol"
"github.com/iteplenky/gophertunnel/minecraft/protocol/packet"
)
const (
contentsJSON = "contents.json"
manifestJSON = "manifest.json"
packIconPNG = "pack_icon.png"
keysSuffix = "_keys.json"
keysFileName = "keys.json"
mcpackExt = ".mcpack"
contentsHeaderSize = 256
)
var contentsHeaderMagic = [4]byte{0xFC, 0xB9, 0xCF, 0x9B}
type contentsEntry struct {
Path string `json:"path"`
Key string `json:"key"`
}
type contentsFile struct {
Content []contentsEntry `json:"content"`
}
type keyEntry struct {
Key string `json:"key"`
Version string `json:"version"`
Name string `json:"name"`
}
// keyStore is a thread-safe key collection that persists to JSON.
type keyStore struct {
mu sync.Mutex
keys map[string]keyEntry
file string
}
func newKeyStore(file string) *keyStore {
return &keyStore{
keys: make(map[string]keyEntry),
file: file,
}
}
// merge adds keys and persists. Persist happens under the mutex so
// concurrent merges can't overwrite each other's snapshot.
func (ks *keyStore) merge(collected map[string]keyEntry) {
ks.mu.Lock()
defer ks.mu.Unlock()
maps.Copy(ks.keys, collected)
if len(ks.keys) == 0 {
return
}
if err := saveKeys(ks.keys, ks.file); err != nil {
fmt.Fprintf(os.Stderr, lang.T("packs.warn.keysSaveFailed"), err)
}
}
func (ks *keyStore) snapshot() map[string]keyEntry {
ks.mu.Lock()
defer ks.mu.Unlock()
return maps.Clone(ks.keys)
}
func (ks *keyStore) count() int {
ks.mu.Lock()
defer ks.mu.Unlock()
return len(ks.keys)
}
// parseResourcePacks decodes a ResourcePacksInfo payload. recover()
// catches protocol.Reader panics on malformed data and returns them
// as errPackBadProtocol.
func parseResourcePacks(payload []byte) (packs []protocol.TexturePackInfo, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%w: payload: %v", errPackBadProtocol, r)
}
}()
pk := &packet.ResourcePacksInfo{}
r := protocol.NewReader(bytes.NewBuffer(payload), 0, false)
pk.Marshal(r)
return pk.TexturePacks, nil
}
// sanitizeServerAddr maps non-alphanumeric to underscore for use in
// filenames (server:port -> safe file stem).
func sanitizeServerAddr(s string) string {
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
return r
}
return '_'
}, s)
}
// sanitizePackName preserves Unicode letters/digits so non-Latin pack
// names ("Кириллический Pack", "日本語パック") survive. Spaces become
// underscores; dashes and underscores are kept; everything else is dropped.
func sanitizePackName(name string) string {
return strings.Map(func(r rune) rune {
switch {
case r == ' ':
return '_'
case r == '_' || r == '-':
return r
case unicode.IsLetter(r), unicode.IsDigit(r):
return r
}
return -1
}, name)
}
// collectKeys extracts encryption keys from ResourcePacksInfo.
// Name field is set to UUID because TexturePackInfo has no pack name -
// the real name is only available post-download via resource.Pack.
func collectKeys(packs []protocol.TexturePackInfo) map[string]keyEntry {
keys := make(map[string]keyEntry)
for _, tp := range packs {
if tp.ContentKey == "" {
continue
}
keys[tp.UUID.String()] = keyEntry{
Key: tp.ContentKey,
Version: tp.Version,
Name: tp.UUID.String(),
}
}
return keys
}
func readPackUUID(packDir string) (string, error) {
data, err := os.ReadFile(filepath.Join(packDir, manifestJSON))
if err != nil {
return "", fmt.Errorf("%w: read: %w", errPackBadManifest, err)
}
var manifest struct {
Header struct {
UUID string `json:"uuid"`
} `json:"header"`
}
if err := json.Unmarshal(data, &manifest); err != nil {
return "", fmt.Errorf("%w: parse: %w", errPackBadManifest, err)
}
if manifest.Header.UUID == "" {
return "", fmt.Errorf("%w: no header.uuid", errPackNoManifest)
}
return manifest.Header.UUID, nil
}
// readPackName returns the pack name from manifest.json, or "".
// Used to rename CDN-downloaded packs from UUID to human-readable.
func readPackName(packDir string) string {
data, err := os.ReadFile(filepath.Join(packDir, manifestJSON))
if err != nil {
return ""
}
var manifest struct {
Header struct {
Name string `json:"name"`
} `json:"header"`
}
if err := json.Unmarshal(data, &manifest); err != nil {
return ""
}
return manifest.Header.Name
}
func saveKeys(keys map[string]keyEntry, path string) error {
data, err := json.MarshalIndent(keys, "", " ")
if err != nil {
return err
}
// Atomic write so the secret keys never persist half-written, and so an
// existing looser-mode file gets retightened (os.WriteFile would not).
return atomicWriteFile(path, "._keys-*.tmp", data, 0600)
}