/
resolve_instructions.go
126 lines (111 loc) · 3.57 KB
/
resolve_instructions.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
package patcher
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type ResolvedInstructions struct {
Instructions []Instruction
BaseUrl *url.URL
VersionName string
}
// productsJson contains the relevant parts of the products.json file.
type productsJson struct {
Games []struct {
ReleaseUrl string `json:"legacy_data_path"`
Tag string `json:"tag"`
} `json:"games"`
}
// releaseJson contains the relevant parts of the release.json file.
type releaseJson struct {
Game struct {
InstructionsHash string `json:"instructions_hash"`
PatchPath string `json:"patch_path"`
Mirrors []struct {
Url string `json:"url"`
} `json:"mirrors"`
VersionName string `json:"version_name"`
} `json:"game"`
}
// ResolveInstructions finds the instructions and URL containing the patch files by looking up a product
// through the root products.json file.
func ResolveInstructions(productsUrl *url.URL, product string) (*ResolvedInstructions, error) {
products, err := fetchJson[productsJson]("products.json", productsUrl)
if err != nil {
return nil, err
}
var releaseUrl *url.URL
for _, game := range products.Games {
if HashEqual(game.Tag, product) {
releaseUrl, err = url.Parse(game.ReleaseUrl)
if err != nil {
return nil, fmt.Errorf("can't convert %q in '%s' to URL: %w", game.ReleaseUrl, productsUrl, err)
}
}
}
if releaseUrl == nil {
return nil, fmt.Errorf("couldn't find game '%s' in '%s'", product, productsUrl)
}
release, err := fetchJson[releaseJson]("release.json", releaseUrl)
if err != nil {
return nil, err
}
if len(release.Game.Mirrors) == 0 {
return nil, fmt.Errorf("there are no mirrors for gmae '%s' in '%s'", product, releaseUrl)
}
// Just fetch the first, there's only one mirror nowadays.
mirrorUrl, err := url.Parse(release.Game.Mirrors[0].Url)
if err != nil {
return nil, fmt.Errorf("can't convert %q in '%s' to URL: %w", release.Game.Mirrors[0].Url, releaseUrl, err)
}
baseUrl := mirrorUrl.JoinPath(release.Game.PatchPath)
instructionsUrl := baseUrl.JoinPath("instructions.json")
instructionsData, err := fetchBytes("instructions.json", instructionsUrl)
if err != nil {
return nil, err
}
checksum := HashBytes(instructionsData)
if !HashEqual(release.Game.InstructionsHash, checksum) {
return nil, fmt.Errorf("'%s' hash mismatch, expected %s got %s", instructionsUrl,
strings.ToUpper(release.Game.InstructionsHash), strings.ToUpper(checksum))
}
instructions, err := DecodeInstructions(instructionsData)
if err != nil {
return nil, fmt.Errorf("failed to decode instructions from '%s': %w", instructionsUrl, err)
}
return &ResolvedInstructions{
BaseUrl: baseUrl,
Instructions: instructions,
VersionName: release.Game.VersionName,
}, nil
}
func fetchBytes(what string, location *url.URL) ([]byte, error) {
resp, err := http.Get(location.String())
if err != nil {
// Error message very likely contains URL already.
return nil, fmt.Errorf("failed to fetch %s: %v", what, err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to fetch %s (status %d)", what, resp.StatusCode)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from '%s': %w", location, err)
}
return data, nil
}
func fetchJson[T any](what string, location *url.URL) (T, error) {
var val T
data, err := fetchBytes(what, location)
if err != nil {
return val, err
}
if err := json.Unmarshal(data, &val); err != nil {
return val, fmt.Errorf("failed to decode response from '%s': %w", location, err)
}
return val, nil
}