// Package goversioninfo creates a syso file which contains Microsoft Version Information and an optional icon.
package goversioninfo
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/akavel/rsrc/binutil"
"github.com/akavel/rsrc/coff"
)
// *****************************************************************************
// JSON and Config
// *****************************************************************************
// ParseJSON parses the given bytes as a VersionInfo JSON.
func (vi *VersionInfo) ParseJSON(jsonBytes []byte) error {
return json.Unmarshal([]byte(jsonBytes), &vi)
}
// VersionInfo data container
type VersionInfo struct {
FixedFileInfo `json:"FixedFileInfo"`
StringFileInfo `json:"StringFileInfo"`
VarFileInfo `json:"VarFileInfo"`
Timestamp bool
Buffer bytes.Buffer
Structure VSVersionInfo
IconPath string `json:"IconPath"`
ManifestPath string `json:"ManifestPath"`
}
// Translation with langid and charsetid.
type Translation struct {
LangID `json:"LangID"`
CharsetID `json:"CharsetID"`
}
// FileVersion with 3 parts.
type FileVersion struct {
Major int
Minor int
Patch int
Build int
}
// FixedFileInfo contains file characteristics - leave most of them at the defaults.
type FixedFileInfo struct {
FileVersion `json:"FileVersion"`
ProductVersion FileVersion
FileFlagsMask string
FileFlags string
FileOS string
FileType string
FileSubType string
}
// VarFileInfo is the translation container.
type VarFileInfo struct {
Translation `json:"Translation"`
}
// StringFileInfo is what you want to change.
type StringFileInfo struct {
Comments string
CompanyName string
FileDescription string
FileVersion string
InternalName string
LegalCopyright string
LegalTrademarks string
OriginalFilename string
PrivateBuild string
ProductName string
ProductVersion string
SpecialBuild string
}
// *****************************************************************************
// Helpers
// *****************************************************************************
// SizedReader is a *bytes.Buffer.
type SizedReader struct {
*bytes.Buffer
}
// Size returns the length of the buffer.
func (s SizedReader) Size() int64 {
return int64(s.Buffer.Len())
}
func str2Uint32(s string) uint32 {
if s == "" {
return 0
}
u, err := strconv.ParseUint(s, 16, 32)
if err != nil {
log.Printf("Error parsing %q as uint32: %v", s, err)
return 0
}
return uint32(u)
}
func padString(s string, zeros int) []byte {
b := make([]byte, 0, len([]rune(s))*2)
for _, x := range s {
tt := int32(x)
b = append(b, byte(tt))
if tt > 255 {
tt = tt >> 8
b = append(b, byte(tt))
} else {
b = append(b, byte(0))
}
}
for i := 0; i < zeros; i++ {
b = append(b, 0x00)
}
return b
}
func padBytes(i int) []byte {
return make([]byte, i)
}
// NewFileVersion parses semver version string into a FileVersion object
func NewFileVersion(version string) (FileVersion, error) {
re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?`)
comps := re.FindStringSubmatch(version)
if len(comps) == 0 {
return FileVersion{}, fmt.Errorf("version expected to start from x.y.z")
}
// First match group is a whole matched string.
comps = comps[1:]
if comps[3] == "" {
comps = comps[:3]
}
nums := make([]int, len(comps))
for i := range nums {
n, err := strconv.Atoi(comps[i])
if err != nil {
return FileVersion{}, fmt.Errorf("%s: %s", comps[i], err)
}
nums[i] = n
}
res := FileVersion{
Major: nums[0],
Minor: nums[1],
Patch: nums[2],
}
if len(nums) == 4 {
res.Build = nums[3]
}
return res, nil
}
func (f FileVersion) getVersionHighString() string {
return fmt.Sprintf("%04x%04x", f.Major, f.Minor)
}
func (f FileVersion) getVersionLowString() string {
return fmt.Sprintf("%04x%04x", f.Patch, f.Build)
}
// GetVersionString returns a string representation of the version
func (f FileVersion) GetVersionString() string {
return fmt.Sprintf("%d.%d.%d.%d", f.Major, f.Minor, f.Patch, f.Build)
}
func (t Translation) getTranslationString() string {
return fmt.Sprintf("%04X%04X", t.LangID, t.CharsetID)
}
func (t Translation) getTranslation() string {
return fmt.Sprintf("%04x%04x", t.CharsetID, t.LangID)
}
// *****************************************************************************
// IO Methods
// *****************************************************************************
// Walk writes the data buffer with hexadecimal data from the structs
func (vi *VersionInfo) Walk() {
// Create a buffer
var b bytes.Buffer
w := binutil.Writer{W: &b}
// Write to the buffer
binutil.Walk(vi.Structure, func(v reflect.Value, path string) error {
if binutil.Plain(v.Kind()) {
w.WriteLE(v.Interface())
}
return nil
})
vi.Buffer = b
}
// WriteSyso creates a resource file from the version info and optionally an icon.
// arch must be an architecture string accepted by coff.Arch, like "386" or "amd64"
func (vi *VersionInfo) WriteSyso(filename string, arch string) error {
var i uint16
newID := func() uint16 {
i++
return i
}
// Create a new RSRC section
rsrc := coff.NewRSRC()
// Set the architecture
err := rsrc.Arch(arch)
if err != nil {
return err
}
// ID 16 is for Version Information
rsrc.AddResource(16, 1, SizedReader{bytes.NewBuffer(vi.Buffer.Bytes())})
// If manifest is enabled
if vi.ManifestPath != "" {
manifest, err := binutil.SizedOpen(vi.ManifestPath)
if err != nil {
return err
}
defer manifest.Close()
id := newID()
rsrc.AddResource(rtManifest, id, manifest)
}
// If icon is enabled
if vi.IconPath != "" {
if err := addIcon(rsrc, vi.IconPath, newID); err != nil {
return err
}
}
rsrc.Freeze()
// Write to file
return writeCoff(rsrc, filename)
}
// WriteHex creates a hex file for debugging version info
func (vi *VersionInfo) WriteHex(filename string) error {
return os.WriteFile(filename, vi.Buffer.Bytes(), 0655)
}
// WriteGo creates a Go file that contains the version info so you can access
// it in the application
func (vi *VersionInfo) WriteGo(filename, packageName string) error {
if len(packageName) == 0 {
packageName = "main"
}
out, err := os.Create(filename)
if err != nil {
return err
}
ffib, err := json.MarshalIndent(vi.FixedFileInfo, "\t", "\t")
if err != nil {
return err
}
sfib, err := json.MarshalIndent(vi.StringFileInfo, "\t", "\t")
if err != nil {
return err
}
vfib, err := json.MarshalIndent(vi.VarFileInfo, "\t", "\t")
if err != nil {
return err
}
replace := "`\" + \"`\" + \"`"
str := "`{\n\t"
str += `"FixedFileInfo":`
str += strings.Replace(string(ffib), "`", replace, -1)
str += ",\n\t"
str += `"StringFileInfo":`
str += strings.Replace(string(sfib), "`", replace, -1)
str += ",\n\t"
str += `"VarFileInfo":`
str += strings.Replace(string(vfib), "`", replace, -1)
str += "\n"
str += "}`"
fmt.Fprintf(out, `// Auto-generated file by goversioninfo. Do not edit.
package %v
import (
"encoding/json"
"github.com/josephspurrier/goversioninfo"
)
func unmarshalGoVersionInfo(b []byte) goversioninfo.VersionInfo {
vi := goversioninfo.VersionInfo{}
json.Unmarshal(b, &vi)
return vi
}
var versionInfo = unmarshalGoVersionInfo([]byte(%v))
`, packageName, string(str))
return nil
}
func writeCoff(coff *coff.Coff, fnameout string) error {
out, err := os.Create(fnameout)
if err != nil {
return err
}
if err = writeCoffTo(out, coff); err != nil {
return fmt.Errorf("error writing %q: %v", fnameout, err)
}
return nil
}
func writeCoffTo(w io.WriteCloser, coff *coff.Coff) error {
bw := binutil.Writer{W: w}
// write the resulting file to disk
binutil.Walk(coff, func(v reflect.Value, path string) error {
if binutil.Plain(v.Kind()) {
bw.WriteLE(v.Interface())
return nil
}
vv, ok := v.Interface().(binutil.SizedReader)
if ok {
bw.WriteFromSized(vv)
return binutil.WALK_SKIP
}
return nil
})
err := bw.Err
if closeErr := w.Close(); closeErr != nil && err == nil {
err = closeErr
}
return err
}