forked from tc-hib/winres
/
version.go
313 lines (281 loc) · 7.03 KB
/
version.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
// Package version provides functions to build a VERSIONINFO structure for Windows applications.
//
// This what Windows displays in the Details tab of file properties.
//
package version
import (
"errors"
"io"
"strings"
"time"
)
const (
Comments = "Comments"
CompanyName = "CompanyName"
FileDescription = "FileDescription"
FileVersion = "FileVersion"
InternalName = "InternalName"
LegalCopyright = "LegalCopyright"
LegalTrademarks = "LegalTrademarks"
OriginalFilename = "OriginalFilename"
PrivateBuild = "PrivateBuild"
ProductName = "ProductName"
ProductVersion = "ProductVersion"
SpecialBuild = "SpecialBuild"
)
type fileType int
const (
App fileType = iota
DLL
Unknown
)
const (
// LangNeutral is the LCID for language agnostic data.
LangNeutral = 0
// LangDefault is the LCID for en-US, and it is the default in many tools and APIs.
LangDefault = 0x409
)
// Info is the main struct of this package.
// Create one as Info{}, use Info.Set() to add key/value pairs,
// set other members, then add it to the resource set with Info.AddTo.
type Info struct {
FileVersion [4]uint16
ProductVersion [4]uint16
Flags versionFlags
Type fileType
Timestamp time.Time
lt langTable
// temporary state
pos int
w io.Writer
}
type versionFlags struct {
Debug bool
Prerelease bool
Patched bool
PrivateBuild bool
SpecialBuild bool
}
type langTable map[uint16]*stringTable
type stringTable map[string]string
// Set sets a key/value pair in the Info structure for a specific locale.
//
// Standard keys are defined as constants in this package: version.ProductName, version.CompanyName, ...
//
// Strings must not contain NUL characters.
//
// Language Code Identifiers (LCID) are listed there: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/
//
// langID may also be 0 for neutral.
func (vi *Info) Set(langID uint16, key string, value string) error {
if vi.lt == nil {
vi.lt = make(langTable)
}
if key == "" {
return errors.New(errEmptyKey)
}
if strings.ContainsRune(key, 0) {
return errors.New(errKeyContainsNUL)
}
if strings.ContainsRune(value, 0) {
return errors.New(errValueContainsNUL)
}
st, ok := vi.lt[langID]
if !ok {
st = &stringTable{}
vi.lt[langID] = st
}
(*st)[key] = value
return nil
}
// Bytes returns the binary representation of the VS_VERSIONINFO struct.
func (vi *Info) Bytes() []byte {
if vi == nil {
return nil
}
return vi.bytes()
}
// FromBytes loads an Info from the binary representation of a VS_VERSIONINFO struct.
func FromBytes(data []byte) (*Info, error) {
return fromBytes(data)
}
// SplitTranslations splits the Info struct by language.
// It returns a map indexed by language ID.
//
// Windows Explorer doesn't seem to search for a proper translation inside the VERSIONINFO struct.
// So one has to embed a whole VERSIONINFO for each language as a resource translation.
func (vi *Info) SplitTranslations() map[uint16]*Info {
if vi == nil {
return nil
}
return vi.splitLangs()
}
// SetProductVersion sets the product version, ensuring this is the only one in the structure.
//
// This should be called after json.Unmarshal to override the version.
func (vi *Info) SetProductVersion(productVersion string) {
vi.ProductVersion = versionStringToArray(productVersion)
if len(vi.lt) == 0 {
vi.Set(LangNeutral, ProductVersion, productVersion)
return
}
for langID := range vi.lt {
vi.Set(langID, ProductVersion, productVersion)
}
}
// SetFileVersion sets the file version, ensuring this is the only one in the structure.
//
// This should be called after json.Unmarshal to override the version.
func (vi *Info) SetFileVersion(fileVersion string) {
vi.FileVersion = versionStringToArray(fileVersion)
if len(vi.lt) == 0 {
vi.Set(LangNeutral, FileVersion, fileVersion)
return
}
for langID := range vi.lt {
vi.Set(langID, FileVersion, fileVersion)
}
}
// MergeTranslations merges several VERSIONINFO structs into one multilingual struct.
//
// The fixed part will be taken in priority from:
// 1. The neutral language (zero)
// 2. The default language (en-US)
// 3. The first language ID in ascending order
//
// Each struct corresponds to one translation, and its language ID will be the one it is mapped to.
//
// This means that each struct is supposed to contain exactly one translation,
// either neutral or of same language ID as it is mapped to.
//
// If a struct contains several translations, those that don't correspond to the map key will be ignored.
//
// If a struct contains one translation with a different language ID,
// it will be imported as if it had been the same value as the map key.
//
func MergeTranslations(translations map[uint16]*Info) *Info {
vi := &Info{}
main := findMainTranslation(translations)
if main == nil {
return vi
}
vi.Flags = main.Flags
vi.Type = main.Type
vi.Timestamp = main.Timestamp
vi.ProductVersion = main.ProductVersion
vi.FileVersion = main.FileVersion
for langID, trans := range translations {
st := trans.lt[langID]
if st == nil || len(*st) == 0 {
st = trans.lt[LangNeutral]
if st == nil || len(*st) == 0 {
st = trans.singleLang()
if st == nil {
continue
}
}
}
for k, v := range *st {
vi.Set(langID, k, v)
}
}
return vi
}
func (vi *Info) singleLang() *stringTable {
var (
seen bool
lang *stringTable
)
for _, st := range vi.lt {
if st != nil && len(*st) != 0 {
if seen {
return nil
}
seen = true
lang = st
}
}
return lang
}
func findMainTranslation(translations map[uint16]*Info) *Info {
main := translations[LangNeutral]
if main != nil {
return main
}
main = translations[LangDefault]
if main != nil {
return main
}
var lang uint16 = 0xFFFF
for k := range translations {
if k < lang {
lang = k
}
}
return translations[lang]
}
func versionStringToArray(v string) [4]uint16 {
var (
part int
ver [4]uint16
i int
)
for i = range v {
if '0' <= v[i] && v[i] <= '9' {
break
}
}
v = v[i:]
for i = range v {
if '0' <= v[i] && v[i] <= '9' {
ver[part] = ver[part]*10 + uint16(v[i]-'0')
} else if v[i] == '.' && part < 3 {
part++
} else {
break
}
}
return ver
}
func (vi *Info) splitLangs() map[uint16]*Info {
m := make(map[uint16]*Info)
defaults := map[string]string{}
if st := vi.lt[LangNeutral]; st != nil {
for k, v := range *st {
if _, ok := defaults[k]; !ok {
defaults[k] = v
}
}
}
if st := vi.lt[LangDefault]; st != nil {
for k, v := range *st {
if _, ok := defaults[k]; !ok {
defaults[k] = v
}
}
}
for _, langID := range vi.lt.sortedKeys() {
for k, v := range *vi.lt[langID] {
if _, ok := defaults[k]; !ok {
defaults[k] = v
}
}
}
for langID, st := range vi.lt {
trans := &Info{
FileVersion: vi.FileVersion,
ProductVersion: vi.ProductVersion,
Flags: vi.Flags,
Type: vi.Type,
Timestamp: vi.Timestamp,
}
for k, v := range defaults {
trans.Set(langID, k, v)
}
for k, v := range *st {
trans.Set(langID, k, v)
}
m[langID] = trans
}
return m
}