I think it's time to init a repo already. xD

This commit is contained in:
Alexander Andreev 2023-10-30 02:18:41 +04:00
commit d60493e1b1
Signed by: Arav
GPG Key ID: D22A817D95815393
25 changed files with 2009 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
bin/

32
Makefile Executable file
View File

@ -0,0 +1,32 @@
TARGET=mccl
ifeq ($(shell go env GOOS),windows)
EXEC_NAME=${TARGET}.exe
else
EXEC_NAME=${TARGET}
endif
DESTDIR:=
PREFIX:=/usr/local
VERSION=0.1.0
FLAGS:=-buildmode=pie -modcacherw -mod=readonly -trimpath
LDFLAGS:= -ldflags "-s -w -X main.programVersion=${VERSION}"
.PHONY: ${TARGET} install uninstall clean
${TARGET}:
ifeq ($(shell go env GOOS),windows)
go build -o bin/$@.exe ${LDFLAGS} ${FLAGS} cmd/$@/main.go
else
go build -o bin/$@ ${LDFLAGS} ${FLAGS} cmd/$@/main.go
endif
install:
install -Dm 0755 bin/${EXEC_NAME} ${DESTDIR}${PREFIX}/bin/${EXEC_NAME}
uninstall:
rm ${DESTDIR}${PREFIX}/bin/${EXEC_NAME}
clean:
rm -f bin/*

26
build/PKGBUILD Normal file
View File

@ -0,0 +1,26 @@
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
pkgname=mccl
pkgver=0.1.0
pkgrel=1
pkgdesc="Console Minecraft launcher"
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
url="https://git.arav.su/Arav/mccl"
license=('GPL3')
makedepends=('go>=1.17')
source=("${pkgver}.tar.gz::https://git.arav.su/Arav/mccl/archive/v${pkgver}.tar.gz")
md5sums=('SKIP')
build() {
cd "$srcdir/$pkgname"
export GOPATH="$srcdir"/gopath
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
make VERSION=$pkgver DESTDIR="$pkgdir" PREFIX="/usr"
}
package() {
cd "$srcdir/$pkgname"
make DESTDIR="$pkgdir" PREFIX="/usr" install
}

5
cmd/mccl/commands/command.go Executable file
View File

@ -0,0 +1,5 @@
package commands
type Command interface {
Run() error
}

View File

@ -0,0 +1,325 @@
package commands
import (
"archive/zip"
"errors"
"fmt"
"io"
"mccl/internal/assets"
"mccl/internal/manifest"
"mccl/internal/version_manifest"
"mccl/pkg/retriever"
"mccl/pkg/util"
"os"
"path"
"runtime"
"strings"
"time"
)
type InstallCommand struct {
Id string
DestDir string
}
func NewInstallCommand(id, destDir string) *InstallCommand {
return &InstallCommand{Id: id, DestDir: destDir}
}
func (ic *InstallCommand) Run() error {
if ic.Id == "" {
return errors.New("an empty Minecraft version was provided")
}
if ic.DestDir == "" {
return errors.New("an empty path was provided")
}
return ic.install_vanilla_client()
}
func (ic *InstallCommand) install_vanilla_client() error {
var err error
var data []byte
if ic.DestDir == "." {
execPath, _ := os.Executable()
ic.DestDir = path.Dir(execPath)
}
if _, err := os.Stat(ic.DestDir); err != nil {
if err := os.Mkdir(ic.DestDir, 0777); err != nil {
return err
}
}
fmt.Print("Retrieving a version_manifest_v2.json... ")
data, err = util.GetFromUrl(version_manifest.VersionManifestUrl, "", "", -1)
if err != nil {
return err
}
versionManifest, err := version_manifest.New(data)
if err != nil {
return err
}
fmt.Println("Done")
var version version_manifest.Version
if ic.Id == "release" || ic.Id == "latest-release" {
version, err = versionManifest.GetLatest(version_manifest.VersionTypeRelease)
if err != nil {
return err
}
} else if ic.Id == "snapshot" || ic.Id == "latest-snapshot" {
version, err = versionManifest.GetLatest(version_manifest.VersionTypeSnapshot)
if err != nil {
return err
}
} else {
version, err = versionManifest.Get(ic.Id)
if err != nil {
return err
}
}
fmt.Printf("Version %s (%s) was chosen.\n", version.Id, version.Type)
fmt.Print("Retrieving a manifest file... ")
manifestPath := path.Join(ic.DestDir, "versions", version.Id, version.Id+".json")
data, err = util.LoadOrDownloadFile(manifestPath, version.Url, version.Sha1, "sha1", -1)
if err != nil {
return err
}
manifst, err := manifest.New(data)
if err != nil {
return err
}
fmt.Println("Done")
if !util.IsFileExist(manifestPath) {
fmt.Print("Writing a manifest file to ", manifestPath, " ... ")
if err := util.WriteFile(manifestPath, data); err != nil {
return err
}
fmt.Println("Done")
} else {
fmt.Println(manifestPath, "does exist. Skipping")
}
clientPath := path.Join(ic.DestDir, "versions", manifst.Id, manifst.Id+".jar")
if !util.IsFileExist(clientPath) {
fmt.Print("Retrieving and writing a client to ", clientPath, " ... ")
data, err = util.LoadOrDownloadFile(clientPath, manifst.Downloads["client"].Url,
manifst.Downloads["client"].Sha1, "sha1", manifst.Downloads["client"].Size)
if err != nil {
return err
}
if err := util.WriteFile(clientPath, data); err != nil {
return err
}
fmt.Println("Done")
} else {
fmt.Println(clientPath, "does exist. Skipping")
}
fmt.Printf("Retrieving an asset index %s.json... ", manifst.AssetIndex.Id)
assetIndexPath := path.Join(ic.DestDir, "assets", "indexes", manifst.AssetIndex.Id+".json")
data, err = util.LoadOrDownloadFile(assetIndexPath, manifst.AssetIndex.Url,
manifst.AssetIndex.Sha1, "sha1", manifst.AssetIndex.Size)
if err != nil {
return err
}
asts, err := assets.New(data)
if err != nil {
return err
}
fmt.Println("Done")
if !util.IsFileExist(assetIndexPath) {
fmt.Printf("Writing an asset index %s to %s...", manifst.AssetIndex.Id, assetIndexPath)
if err := util.WriteFile(assetIndexPath, data); err != nil {
return err
}
fmt.Println("Done")
} else {
fmt.Printf("%s does exist. Skip writing\n", assetIndexPath)
}
fmt.Print("Retrieving assets...")
assetsDir := path.Join(ic.DestDir, "assets", "objects")
ar := retriever.New()
defer ar.Close()
assetsCount := len(asts.Objects)
var assetItems []retriever.Retrievable
for _, v := range asts.Objects {
assetItems = append(assetItems, v)
}
go func() {
if err := ar.Run(assetItems, assets.RetrieveAsset, assetsDir); err != nil {
fmt.Println("\n", err)
os.Exit(1)
}
}()
for written, cnt := 0, 0; cnt < assetsCount; {
if len(ar.GetErrorChan()) > 0 {
return <-ar.GetErrorChan()
}
if cnt < assetsCount {
written += <-ar.GetDoneChan()
}
cnt++
fmt.Printf("\rRetrieving assets... %d/%d (%d/%d)", written, manifst.AssetIndex.TotalSize, cnt, assetsCount)
}
fmt.Printf("\nRetrieving assets... Done\n")
if manifst.Logging.Client.Argument != "" {
logConfPath := path.Join(ic.DestDir, "assets", "log_configs", manifst.Logging.Client.File.Id)
if !util.IsFileExist(logConfPath) {
fmt.Printf("Retrieving and writing %s...", logConfPath)
data, err = util.LoadOrDownloadFile(logConfPath, manifst.Logging.Client.File.Url,
manifst.Logging.Client.File.Sha1, "sha1", manifst.Logging.Client.File.Size)
if err != nil {
return err
}
if err := util.WriteFile(logConfPath, data); err != nil {
return err
}
fmt.Println("Done")
} else {
fmt.Printf("%s does exist. Skip writing\n", logConfPath)
}
}
fmt.Print("Retrieving libraries...")
librariesDir := path.Join(ic.DestDir, "libraries")
lr := retriever.New()
defer lr.Close()
var libraryItems []retriever.Retrievable
var totalLibrariesSize int64 = 0
for _, lib := range manifst.Libraries {
if lib.CheckRules() {
files := lib.ToFile()
for _, f := range files {
totalLibrariesSize += f.Size
libraryItems = append(libraryItems, f)
}
}
}
go func() {
if err := lr.Run(libraryItems, manifest.RetrieveLibrary, librariesDir); err != nil {
fmt.Println("\n", err)
os.Exit(1)
}
}()
librariesCount := len(libraryItems)
for written, cnt := 0, 0; cnt < librariesCount; {
if len(lr.GetErrorChan()) > 0 {
return <-lr.GetErrorChan()
}
if cnt < librariesCount {
written += <-lr.GetDoneChan()
}
cnt++
fmt.Printf("\rRetrieving libraries... %d/%d (%d/%d)",
written, totalLibrariesSize, cnt, librariesCount)
}
fmt.Printf("\nRetrieving libraries... Done\n")
releaseTime, err := time.Parse(time.RFC3339, manifst.ReleaseTime)
if err != nil {
releaseTime, err = time.Parse("2006-01-02T15:04:05Z0700", manifst.ReleaseTime)
if err != nil {
return err
}
}
firstVerWithNoExtract, _ := time.Parse(time.DateOnly, "2017-10-25")
if releaseTime.Before(firstVerWithNoExtract) {
nativesDir := path.Join(ic.DestDir, "versions", manifst.Id, "natives")
if err := os.MkdirAll(nativesDir, 0777); err != nil {
return err
}
fmt.Printf("Extracting native libraries to %s ... ", nativesDir)
for _, lib := range libraryItems {
l := lib.(util.File)
if !strings.Contains(l.GetName(), "natives") {
continue
}
zipFile, err := zip.OpenReader(path.Join(librariesDir, l.Path))
if err != nil {
return err
}
defer zipFile.Close()
for _, f := range zipFile.File {
if strings.HasPrefix(f.Name, "META-INF/") {
continue
}
if strings.Contains(f.Name, "64") && runtime.GOARCH != "amd64" {
continue
}
switch runtime.GOARCH {
case "386":
if strings.Contains(f.Name, "64") {
continue
}
case "amd64":
if !strings.Contains(f.Name, "64") {
continue
}
}
if util.IsFileExist(path.Join(nativesDir, f.Name)) {
continue
}
frc, err := f.Open()
if err != nil {
return err
}
defer frc.Close()
out, err := os.OpenFile(path.Join(nativesDir, f.Name), os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return err
}
defer out.Close()
written, err := io.Copy(out, frc)
if err != nil {
return fmt.Errorf("failed to extract file %s: %s", f.Name, err)
} else if written != int64(f.UncompressedSize64) {
return fmt.Errorf("failed to extract file %s: size is %d, but %d was written",
f.Name, f.UncompressedSize64, written)
}
}
}
fmt.Println("Done")
}
return nil
}

View File

@ -0,0 +1,33 @@
package commands
import (
"fmt"
"os"
"path"
)
type ListCommand struct {
GameDir string
}
func NewListCommand(gameDir string) *ListCommand {
return &ListCommand{GameDir: gameDir}
}
func (lc *ListCommand) Run() error {
versionsDir := path.Join(lc.GameDir, "versions")
if _, err := os.Stat(versionsDir); err != nil {
return fmt.Errorf("versions directory wasn't found inside %s", lc.GameDir)
}
entries, err := os.ReadDir(path.Join(lc.GameDir, "versions"))
if err != nil {
return err
}
fmt.Println("There are following versions installed:")
for _, entry := range entries {
if entry.IsDir() {
fmt.Println(entry.Name())
}
}
return nil
}

163
cmd/mccl/commands/run_command.go Executable file
View File

@ -0,0 +1,163 @@
package commands
import (
"errors"
"fmt"
"mccl/internal/manifest"
mcclprofile "mccl/internal/mccl_profile"
"mccl/pkg/util"
"os"
"os/exec"
"path"
"regexp"
"strings"
)
type RunCommand struct {
Id string
GameDir string
Username string
AuthUuid string
JavaXmx string
UseProfile bool
}
func NewRunCommand(id, gameDir, user, uuid, jxmx string, useProfile bool) *RunCommand {
return &RunCommand{Id: id, GameDir: gameDir, Username: user, AuthUuid: uuid, JavaXmx: jxmx, UseProfile: useProfile}
}
func (rc *RunCommand) Run() error {
if rc.Id == "" {
return errors.New("an empty Minecraft version was provided")
}
if err := os.Chdir(rc.GameDir); err != nil {
return err
}
if rc.UseProfile {
if util.IsFileExist(path.Join(rc.GameDir, mcclprofile.ProfileFileName)) {
p, err := mcclprofile.Load(rc.GameDir)
if err != nil {
return err
}
rc.Username = p.Username
rc.AuthUuid = p.Uuid
rc.JavaXmx = p.JavaXmx
} else {
p := mcclprofile.Profile{
Username: rc.Username,
Uuid: rc.AuthUuid,
JavaXmx: rc.JavaXmx}
if err := p.Store(rc.GameDir); err != nil {
return err
}
}
}
return rc.run_client()
}
func (rc *RunCommand) run_client() error {
manifestPath := path.Join(rc.GameDir, "versions", rc.Id, rc.Id+".json")
data, err := util.ReadFile(manifestPath)
if err != nil {
return err
}
manifst, err := manifest.New(data)
if err != nil {
return err
}
if manifst.InheritsFrom != "" {
manifestPath := path.Join(rc.GameDir, "versions", manifst.InheritsFrom, manifst.InheritsFrom+".json")
data, err := util.ReadFile(manifestPath)
if err != nil {
return err
}
inherit, err := manifest.New(data)
if err != nil {
return err
}
if err := manifst.InheritFrom(inherit); err != nil {
return err
}
}
p := make(map[string]string)
p["auth_player_name"] = rc.Username
p["version_name"] = manifst.Id
p["game_directory"] = rc.GameDir
p["assets_root"] = "assets"
p["game_assets"] = p["assets_root"]
p["assets_index_name"] = manifst.AssetIndex.Id
p["auth_uuid"] = rc.AuthUuid
p["auth_access_token"] = "null"
p["auth_session"] = p["auth_access_token"]
p["clientid"] = manifst.Id
p["auth_xuid"] = "null"
p["user_type"] = "legacy"
p["version_type"] = manifst.Type
p["natives_directory"] = path.Join("", "versions", manifst.Id, "natives")
p["launcher_name"] = "mccl"
p["launcher_version"] = "0.1.0"
p["classpath"] = manifst.BuildClasspath("", "versions")
if manifst.Logging.Client.File.Id != "" {
p["logging_path"] = path.Join("assets", "log_configs", manifst.Logging.Client.File.Id)
}
if strings.Contains(manifst.Id, "forge") {
p["library_directory"] = "libraries"
p["classpath_separator"] = util.PathSeparator()
}
cl := manifst.BuildCommandLine(p)
if rc.JavaXmx != "" {
ok, err := regexp.MatchString("^\\d+(K|k|M|m|G|g|T|t)$", rc.JavaXmx)
if ok && err == nil {
rc.JavaXmx = "-Xmx" + rc.JavaXmx
tcl := []string{}
tcl = append(tcl, rc.JavaXmx)
cl = append(tcl, cl...)
} else if err != nil || !ok {
if err != nil {
fmt.Printf("An error occured while parsing Java -Xmx parameter: %s. Game's default will be used. ", err)
} else {
fmt.Printf("Mismatch occured while parsing Java -Xmx parameter. Game's default will be used. ")
}
fmt.Printf("Right form is <number>Kk|Mm|Gg|Tt, e.g. 4G.")
}
}
javaHome, err := util.LocateJavaHome(manifst.JavaVersion.Component, manifst.JavaVersion.MajorVersion)
if err != nil {
return err
}
cmd := exec.Command(path.Join(javaHome, "bin", "java"), cl...)
// stdout, err := cmd.StdoutPipe()
// cmd.Stderr = cmd.Stdout
// if err != nil {
// return err
// }
// defer stdout.Close()
// scanner := bufio.NewScanner(stdout)
// go func() {
// for scanner.Scan() {
// fmt.Println(scanner.Text())
// }
// }()
fmt.Printf("Minecraft version %s is started with usename %s and player's UUID %s.\n",
manifst.Id, rc.Username, rc.AuthUuid)
if err := cmd.Run(); err != nil && (err.Error() != "exec: already started") {
return err
}
return nil
}

232
cmd/mccl/main.go Executable file
View File

@ -0,0 +1,232 @@
package main
import (
"fmt"
"mccl/cmd/mccl/commands"
mcclprofile "mccl/internal/mccl_profile"
"os"
)
var programVersion string
func main() {
args, err := parseArguments(os.Args[1:])
if err != nil {
fmt.Fprintln(os.Stderr, err)
usage()
os.Exit(1)
}
if args.ShowVersion {
version()
}
if args.ShowHelp {
usage()
}
if args.ShowHelp || args.ShowVersion {
os.Exit(0)
}
if len(os.Args) < 2 {
usage()
os.Exit(0)
}
var cmd commands.Command
switch args.Command {
case "install":
fallthrough
case "i":
cmd = commands.NewInstallCommand(args.GameVersion, args.Directory)
case "run":
fallthrough
case "r":
cmd = commands.NewRunCommand(args.GameVersion, args.Directory,
args.Username, args.Uuid, args.JavaXmx, args.UseProfile)
case "list":
fallthrough
case "l":
cmd = commands.NewListCommand(args.Directory)
default:
usage()
os.Exit(0)
}
exitCode := 0
if err := cmd.Run(); err != nil {
fmt.Println(err)
exitCode = 1
}
if args.DemandPressEnter {
fmt.Fprint(os.Stderr, "Press the enter key to continue...")
fmt.Scanln()
}
os.Exit(exitCode)
}
func version() {
fmt.Fprintf(os.Stderr, "mccl ver. %s\nCopyright (c) 2023 Alexander \"Arav\" Andreev <me@arav.su>\n", programVersion)
fmt.Fprintln(os.Stderr, "License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.")
}
func usage() {
fmt.Fprintln(os.Stderr, "Usage: mccl [<common options>]... command arguments... [options]...")
fmt.Fprintln(os.Stderr, "Common options:\n"+
" -h,--help show help\n"+
" -v,--version show version\n"+
" --demand-press-enter\n"+
" ask to press the enter key before closing\n"+
"Commands:\n"+
" \033[1minstall|i\033[0m version directory\n"+
" Install any vanilla version (for mod loaders use theirs installers)\n"+
" \033[1mrun|r\033[0m version directory [-u username] [-U UUID] [-java-Xmx memory] [--profile]\n"+
" Run vanilla, forge, fabric, quilt, the others aren't tested\n"+
" \033[1mlist|l\033[0m directory\n"+
" List all installed versions in directory\n"+
"Explanation of arguments and options for commands:\n"+
" version minecraft's version \n"+
" directory a path to where Minecraft will be installed\n"+
" -u,--username username\n"+
" player's username (Anonymous by default)\n"+
" -U,--uuid UUID\n"+
" player's account UUID (all zeroes by default)\n"+
" --java-Xmx memory\n"+
" java's -Xmx param, e.g. 4G\n"+
" --profile\n"+
" load/save username, UUID, and -Xmx from/to a file.\n"+
" Once saved, you can ommit -u,-U and --java-Xmx,\n"+
" since they are stored in", mcclprofile.ProfileFileName)
}
type arguments struct {
ShowVersion bool
ShowHelp bool
DemandPressEnter bool
Command string
GameVersion string
Directory string
Username string
Uuid string
JavaXmx string
UseProfile bool
}
type errNoOptArg struct {
ArgName string
}
func (e errNoOptArg) Error() string {
return fmt.Sprintf("%s option demands an argument, but it wasn't provided", e.ArgName)
}
func parseArguments(args []string) (parsed arguments, err error) {
parsed.Username = "Anonymous"
parsed.Directory = "."
parsed.Uuid = "00000000-0000-0000-0000-000000000000"
for i := 0; i < len(args); i++ {
if len(args[i]) > 2 && args[i][0] == '-' && args[i][1] == '-' {
switch args[i][2:] {
case "version":
parsed.ShowVersion = true
case "help":
parsed.ShowHelp = true
case "demand-press-enter":
parsed.DemandPressEnter = true
case "username":
if len(args) == i+1 || args[i+1][0] == '-' {
err = errNoOptArg{ArgName: args[i]}
return
}
parsed.Username = args[i+1]
i++
case "uuid":
if len(args) == i+1 || args[i+1][0] == '-' {
err = errNoOptArg{ArgName: args[i]}
return
}
parsed.Uuid = args[i+1]
i++
case "java-Xmx":
if len(args) == i+1 || args[i+1][0] == '-' {
err = errNoOptArg{ArgName: args[i]}
return
}
parsed.JavaXmx = args[i+1]
i++
case "profile":
parsed.UseProfile = true
default:
err = fmt.Errorf("an unknown option %s was provided", args[i])
return
}
} else if args[i][0] == '-' && len(args[i]) > 1 {
for _, option := range args[i][1:] {
switch option {
case 'v':
parsed.ShowVersion = true
case 'h':
parsed.ShowHelp = true
case 'u':
if len(args) == i+1 || args[i+1][0] == '-' {
err = errNoOptArg{ArgName: args[i]}
return
}
parsed.Username = args[i+1]
i++
case 'U':
if len(args) == i+1 || args[i+1][0] == '-' {
err = errNoOptArg{ArgName: args[i]}
return
}
parsed.Uuid = args[i+1]
i++
default:
err = fmt.Errorf("an unknown option %c was provided", option)
return
}
}
} else {
if parsed.Command != "" {
err = fmt.Errorf("only one command should present")
return
}
switch parsed.Command = args[i]; parsed.Command {
case "i":
fallthrough
case "install":
fallthrough
case "r":
fallthrough
case "run":
if len(args) <= i+3 || args[i+1][0] == '-' || args[i+2][0] == '-' {
err = fmt.Errorf("not enough arguments was provided")
return
}
parsed.GameVersion = args[i+1]
parsed.Directory = args[i+2]
i += 2
case "l":
fallthrough
case "list":
if len(args) <= i+2 || args[i+1][0] == '-' {
err = fmt.Errorf("not enough arguments was provided")
return
}
parsed.Directory = args[i+1]
i++
default:
err = fmt.Errorf("an unknown command %s was provided", parsed.Command)
return
}
}
}
return
}

5
go.mod Executable file
View File

@ -0,0 +1,5 @@
module mccl
go 1.17
require golang.org/x/sys v0.13.0

2
go.sum Executable file
View File

@ -0,0 +1,2 @@
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

124
internal/assets/assets.go Executable file
View File

@ -0,0 +1,124 @@
package assets
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"mccl/pkg/retriever"
"net/http"
"os"
"path"
)
// assetDownloadUrl is a base URL for an asset to be retrieved from, first %s
// is a directory which name is first two characters of a hash that goes
// under the second %s.
const assetDownloadUrl = "https://resources.download.minecraft.net/%s/%s"
// Asset represents a single asset file, that is some resource like a texture,
// or a sound, and so on.
type Asset struct {
Hash string `json:"hash"`
Size int64 `json:"size"`
}
// url forms a full URL to an asset.
func (a *Asset) url() string {
return fmt.Sprintf(assetDownloadUrl, a.Hash[:2], a.Hash)
}
// path forms a relative path to an asset.
func (a *Asset) path() string {
return path.Join(a.Hash[:2], a.Hash)
}
// Retrieve is a method of a Retrievable interface that is used to download
// an asset from Mojang's server. It performs the checks for a valid size
// and hash. Returns a slice of bytes an asset consists of or an error
// if something gone wrong.
func (a Asset) Retrieve() ([]byte, error) {
resp, err := http.Get(a.url())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to download a file. Status code is %d", resp.StatusCode)
} else if resp.ContentLength != a.Size {
return nil, fmt.Errorf("response size mismatch. %d != %d", resp.ContentLength, a.Size)
}
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if l := len(buf); l != int(a.Size) {
return nil, fmt.Errorf("file size mismatch. %d != %d", l, a.Size)
}
h := sha1.New()
if _, err := h.Write(buf); err != nil {
return nil, err
}
if hex.EncodeToString(h.Sum(nil)) != a.Hash {
return nil, errors.New("hash mismatch")
}
return buf, nil
}
// Assets represents an asset index file.
type Assets struct {
Objects map[string]Asset `json:"objects"`
}
// New returns a new Assets that is being unmarshaled from a JSON data.
func New(data []byte) (*Assets, error) {
a := &Assets{}
if err := json.Unmarshal(data, a); err != nil {
return nil, err
}
return a, nil
}
// RetrieveAsset is a special retriever.WorkerFunc function that is used to
// check for an existency of an asset, and, if not, to retrieve it and write
// it to an actual file.
func RetrieveAsset(item retriever.Retrievable, basePath string) (int, error) {
asset := item.(Asset)
p := path.Join(basePath, asset.path())
if s, err := os.Stat(p); err == nil && s.Size() == asset.Size {
return int(s.Size()), nil
}
data, err := item.Retrieve()
if err != nil {
return 0, err
}
if err := os.MkdirAll(path.Dir(p), 0777); err != nil {
return 0, err
}
tf, err := os.Create(p)
if err != nil {
return 0, err
}
defer tf.Close()
written, err := tf.Write(data)
if err != nil {
return 0, err
} else if written != len(data) {
return 0, fmt.Errorf("written bytes length mismatch. %d != %d", written, len(data))
}
return written, nil
}

60
internal/manifest/argument.go Executable file
View File

@ -0,0 +1,60 @@
package manifest
import (
"bytes"
"encoding/json"
)
// Argument is a special struct that was made to handle a mixed-typed array
// of arguments in a manifest.
//
// An argument in a manifest could be:
// - a single string
// - an object consists of rules array field and a value field that could be
// a single string or an array of strings.
type Argument struct {
Rules []Rule `json:"rules"`
Value []string `json:"value"`
}
// CheckRules returns true if all rules passed and a feature set in a rule if
// applicable, a FeatureUnknown otherwise.
func (a Argument) CheckRules() (ok bool, feature Feature) {
feature = FeatureUnknown
for _, r := range a.Rules {
ok, feature = r.Check()
if !ok {
return false, feature
}
}
return true, feature
}
// UnmarshalJSON had to be implemented because of mixed-typed nature of an
// arguments array in a manifest.
func (a *Argument) UnmarshalJSON(data []byte) error {
if data[0] == '"' {
a.Value = append(a.Value, string(data[1:len(data)-1]))
} else if data[0] == '{' && bytes.Contains(data, []byte("value\": \"")) {
singleValArg := struct {
Rules []Rule `json:"rules"`
Value string `json:"value"`
}{}
if err := json.Unmarshal(data, &singleValArg); err != nil {
return err
}
a.Rules = singleValArg.Rules
a.Value = append(a.Value, singleValArg.Value)
} else if data[0] == '{' {
multiValArg := struct {
Rules []Rule `json:"rules"`
Value []string `json:"value"`
}{}
if err := json.Unmarshal(data, &multiValArg); err != nil {
return err
}
a.Rules = multiValArg.Rules
a.Value = multiValArg.Value
}
return nil
}

View File

@ -0,0 +1,16 @@
package manifest
import (
"encoding/json"
"testing"
)
func TestArgument(t *testing.T) {
s := []byte("{\"rules\": [{ \"action\": \"allow\", \"os\": { \"name\": \"osx\" } }], \"value\":[\"-XstartOnFirstThread\"]}")
a := Argument{}
if err := json.Unmarshal(s, &a); err != nil {
t.Fatal(err)
}
ok, f := a.CheckRules()
t.Log(a, ok, f)
}

136
internal/manifest/library.go Executable file
View File

@ -0,0 +1,136 @@
package manifest
import (
"fmt"
"mccl/pkg/retriever"
"mccl/pkg/util"
"os"
"path"
"runtime"
"strings"
)
type Library struct {
Downloads struct {
Artifact util.File `json:"artifact"`
Classifiers map[string]util.File `json:"classifiers"`
} `json:"downloads"`
Name string `json:"name"`
Url string `json:"url"`
Rules []Rule `json:"rules"`
}
func (l *Library) Path() string {
if l.Downloads.Artifact.Path != "" {
return l.Downloads.Artifact.Path
}
return l.pathFromName()
}
func (l *Library) ToFile() []util.File {
var f []util.File
if l.Downloads.Artifact.Url != "" {
f = append(f, l.Downloads.Artifact)
} else if l.Url != "" {
lf := util.File{
Id: l.Name,
Url: path.Join(l.Url, l.pathFromName()),
Size: -1,
Path: l.pathFromName(),
}
f = append(f, lf)
}
if natives := l.Natives(); natives != nil {
f = append(f, *natives)
}
return f
}
func (l *Library) pathFromName() string {
parts := strings.Split(l.Name, ":")
parts = append(strings.Split(parts[0], "."), parts[1:]...)
pth := path.Join(parts...)
jarName := strings.Join([]string{parts[len(parts)-2], parts[len(parts)-1]}, "-") + ".jar"
return path.Join(pth, jarName)
}
func RetrieveLibrary(item retriever.Retrievable, destDir string) (int, error) {
l := item.(util.File)
p := path.Join(destDir, l.Path)
if s, err := os.Stat(p); err == nil && s.Size() == l.Size {
return int(l.Size), nil
}
data, err := item.Retrieve()
if err != nil {
return 0, err
}
if err := os.MkdirAll(path.Dir(p), 0777); err != nil {
return 0, err
}
tf, err := os.Create(p)
if err != nil {
return 0, err
}
defer tf.Close()
written, err := tf.Write(data)
if err != nil {
return 0, err
} else if written != len(data) {
return 0, fmt.Errorf("written bytes length mismatch. %d != %d", written, len(data))
}
return written, nil
}
func (l *Library) CheckRules() bool {
if strings.Contains(l.Name, "natives") {
nat := strings.SplitAfter(l.Name, ":")[3]
nats := strings.Split(nat, "-")
archR := Rule{Action: ActionAllow}
if len(nats) == 3 {
archR.Os.Arch = nats[2]
l.Rules = append(l.Rules, archR)
}
}
for _, r := range l.Rules {
if ok, _ := r.Check(); !ok {
return false
}
}
return true
}
func (l *Library) Natives() *util.File {
if !l.CheckRules() {
return nil
}
var nativeOs string
switch v := runtime.GOOS; v {
case "windows":
fallthrough
case "linux":
nativeOs = "natives-" + v
case "darwin":
nativeOs = "natives-macos"
}
if len(l.Downloads.Classifiers) > 0 {
if v, ok := l.Downloads.Classifiers[nativeOs]; ok {
return &v
}
}
return nil
}

200
internal/manifest/manifest.go Executable file
View File

@ -0,0 +1,200 @@
package manifest
import (
"encoding/json"
"fmt"
"mccl/pkg/util"
"path"
"strings"
)
type Manifest struct {
InheritsFrom string `json:"inheritsFrom"`
Arguments struct {
Game []Argument `json:"game"`
Jvm []Argument `json:"jvm"`
} `json:"arguments"`
AssetIndex struct {
util.File
TotalSize int `json:"totalSize"`
} `json:"assetIndex"`
Assets string `json:"assets"`
ComplianceLevel int `json:"complianceLevel"`
Downloads map[string]util.File `json:"downloads"`
Id string `json:"id"`
JavaVersion struct {
Component string `json:"component"`
MajorVersion int `json:"majorVersion"`
} `json:"javaVersion"`
Libraries []Library `json:"libraries"`
Logging struct {
Client struct {
Argument string `json:"argument"`
File util.File `json:"file"`
Type string `json:"type"`
} `json:"client"`
} `json:"logging"`
MainClass string `json:"mainClass"`
MinecraftArguments string `json:"minecraftArguments"`
MinimumLauncherVersion int `json:"minimumLauncherVersion"`
ReleaseTime string `json:"releaseTime"`
Time string `json:"time"`
Type string `json:"type"`
}
// New returns a new Manifest for a specific Minecraft version.
func New(data []byte) (*Manifest, error) {
m := &Manifest{}
if err := json.Unmarshal(data, m); err != nil {
return nil, err
}
return m, nil
}
func (m *Manifest) BuildCommandLine(properties map[string]string) (arguments []string) {
if m.MinecraftArguments != "" {
arguments = append(arguments, fmt.Sprintf("-Djava.library.path=%s", properties["natives_directory"]))
arguments = append(arguments, "-cp")
arguments = append(arguments, properties["classpath"])
arguments = append(arguments, m.MainClass)
args := strings.Split(m.MinecraftArguments, " ")
for _, arg := range args {
if strings.HasPrefix(arg, "${") {
propName := arg[2 : len(arg)-1]
arg = properties[propName]
}
arguments = append(arguments, arg)
}
return arguments
}
var allArgs []Argument
allArgs = append(allArgs, m.Arguments.Jvm...)
if m.Logging.Client.Argument != "" {
logConf := strings.Split(m.Logging.Client.Argument, "${")[0]
logConf += properties["logging_path"]
allArgs = append(allArgs, Argument{Value: []string{logConf}})
}
allArgs = append(allArgs, Argument{Value: []string{m.MainClass}})
allArgs = append(allArgs, m.Arguments.Game...)
for _, arg := range allArgs {
ok, feature := arg.CheckRules()
if !ok {
continue
} else if feature != FeatureUnknown {
if _, ok := properties[feature.String()]; !ok {
continue
}
}
for _, v := range arg.Value {
if strings.Contains(v, "${library_directory}") {
v = strings.ReplaceAll(v, "${library_directory}", properties["library_directory"])
v = strings.ReplaceAll(v, "${classpath_separator}", properties["classpath_separator"])
} else if i := strings.Index(v, "${"); i != -1 {
j := strings.Index(v, "}")
propName := v[i+2 : j]
switch propName {
case "version_name":
if strings.Contains(m.Id, "forge") {
v = strings.ReplaceAll(v, "${version_name}", m.InheritsFrom)
} else {
v = strings.ReplaceAll(v, "${version_name}", m.Id)
}
case "assets_index_name":
v = m.Assets
case "version_type":
v = m.Type
default:
v = strings.Replace(v, v[i:j+1], properties[propName], 1)
}
}
arguments = append(arguments, v)
}
}
return arguments
}
func (m *Manifest) BuildClasspath(baseDir, clientPath string) string {
var sb []string = make([]string, 0, len(m.Libraries))
for _, lib := range m.Libraries {
if lib.CheckRules() {
sb = append(sb, path.Join(baseDir, "libraries", lib.Path()))
if natives := lib.Natives(); natives != nil {
sb = append(sb, path.Join(baseDir, "libraries", natives.Path))
}
}
}
if m.InheritsFrom != "" {
sb = append(sb, path.Join(clientPath, m.InheritsFrom, m.InheritsFrom+".jar"))
} else {
sb = append(sb, path.Join(clientPath, m.Id, m.Id+".jar"))
}
return strings.Join(sb, util.PathSeparator())
}
func (m *Manifest) InheritFrom(parent *Manifest) error {
if parent == nil {
return nil
}
m.Arguments.Game = append(parent.Arguments.Game, m.Arguments.Game...)
m.Arguments.Jvm = append(parent.Arguments.Jvm, m.Arguments.Jvm...)
if m.AssetIndex.TotalSize == 0 {
m.AssetIndex.File = parent.AssetIndex.File
m.AssetIndex.TotalSize = parent.AssetIndex.TotalSize
}
if m.Assets == "" {
m.Assets = parent.Assets
}
if m.ComplianceLevel == 0 {
m.ComplianceLevel = parent.ComplianceLevel
}
if m.Downloads == nil {
m.Downloads = make(map[string]util.File)
}
for k, v := range parent.Downloads {
m.Downloads[k] = v
}
if m.Id == "" {
m.Id = parent.Id
}
if m.JavaVersion.Component != "" {
m.JavaVersion.Component = parent.JavaVersion.Component
m.JavaVersion.MajorVersion = parent.JavaVersion.MajorVersion
}
m.Libraries = append(m.Libraries, parent.Libraries...)
if m.Logging.Client.Argument == "" {
m.Logging.Client.Argument = parent.Logging.Client.Argument
m.Logging.Client.Type = parent.Logging.Client.Type
m.Logging.Client.File = parent.Logging.Client.File
}
if m.MainClass == "" {
m.MainClass = parent.MainClass
}
if m.MinecraftArguments == "" {
m.MinecraftArguments = parent.MinecraftArguments
}
return nil
}

122
internal/manifest/rule.go Executable file
View File

@ -0,0 +1,122 @@
package manifest
import (
"mccl/pkg/util"
"regexp"
"runtime"
)
type Feature int
const (
FeatureIsDemoUser Feature = iota
FeatureHasCustomResolution
FeatureHasQuickPlaysSupport
FeatureIsQuickPlaySingleplayer
FeatureIsQuickPlayMultiplayer
FeatureIsQuickPlayRealms
FeatureUnknown
)
func (f Feature) String() string {
switch f {
case FeatureIsDemoUser:
return "is_demo_user"
case FeatureHasCustomResolution:
return "has_custom_resolution"
case FeatureHasQuickPlaysSupport:
return "has_quick_plays_support"
case FeatureIsQuickPlayMultiplayer:
return "is_quick_play_multiplayer"
case FeatureIsQuickPlayRealms:
return "is_quick_play_realms"
case FeatureIsQuickPlaySingleplayer:
return "is_quick_play_singleplayer"
default:
return ""
}
}
type Action string
const (
ActionAllow Action = "allow"
ActionDisallow Action = "disallow"
ActionUnknown Action = ""
)
type Rule struct {
Action Action `json:"action"`
Os struct {
Name string `json:"name"`
Arch string `json:"arch"`
Version string `json:"version"`
} `json:"os"`
Features struct {
IsDemoUser bool `json:"is_demo_user"`
HasCustomResolution bool `json:"has_custom_resolution"`
HasQuickPlaysSupport bool `json:"has_quick_plays_support"`
IsQuickPlaySingleplayer bool `json:"is_quick_play_singleplayer"`
IsQuickPlayMultiplayer bool `json:"is_quick_play_multiplayer"`
IsQuickPlayRealms bool `json:"is_quick_play_realms"`
} `json:"features"`
}
func (r *Rule) GetFeature() Feature {
switch {
case r.Features.IsDemoUser:
return FeatureIsDemoUser
case r.Features.HasCustomResolution:
return FeatureHasCustomResolution
case r.Features.HasQuickPlaysSupport:
return FeatureHasQuickPlaysSupport
case r.Features.IsQuickPlayMultiplayer:
return FeatureIsQuickPlayMultiplayer
case r.Features.IsQuickPlaySingleplayer:
return FeatureIsQuickPlaySingleplayer
case r.Features.IsQuickPlayRealms:
return FeatureIsQuickPlayRealms
default:
return FeatureUnknown
}
}
func (r *Rule) Check() (bool, Feature) {
feature := r.GetFeature()
osArchOk := true
switch r.Os.Arch {
case "x86":
osArchOk = r.Action == ActionAllow && runtime.GOARCH == "386"
case "x86_64":
osArchOk = r.Action == ActionAllow && runtime.GOARCH == "amd64"
case "arm64":
osArchOk = r.Action == ActionAllow && runtime.GOARCH == "arm64"
}
osName := runtime.GOOS
if osName == "darwin" {
osName = "osx"
}
osNameOk := true
if r.Os.Name != "" {
if r.Action == ActionAllow {
osNameOk = r.Os.Name == osName
} else {
osNameOk = r.Os.Name != osName
}
}
osVersionOk := true
if r.Os.Name == osName && r.Os.Version != "" {
ver, err := util.OSVersion()
if err != nil {
return false, feature
}
ok, _ := regexp.MatchString(r.Os.Version, ver)
osVersionOk = ok && r.Action == ActionAllow
}
return osArchOk && osNameOk && osVersionOk, feature
}

49
internal/manifest/rule_test.go Executable file
View File

@ -0,0 +1,49 @@
package manifest
import (
"runtime"
"testing"
)
func TestRuleOsName(t *testing.T) {
macos := Rule{Action: ActionAllow}
macos.Os.Name = "osx"
if ok, _ := macos.Check(); runtime.GOOS != "darwin" && ok {
t.Fatal("Rule.Os.Name == 'osx', but is true on ", runtime.GOOS)
}
linux := Rule{Action: ActionAllow}
linux.Os.Name = "linux"
if ok, _ := linux.Check(); runtime.GOOS != "linux" && ok {
t.Fatalf("Rule.Os.Name == '%s', but is true on %s\n", linux.Os.Name, runtime.GOOS)
}
windows := Rule{Action: ActionAllow}
windows.Os.Name = "windows"
if ok, _ := windows.Check(); runtime.GOOS != "windows" && ok {
t.Fatalf("Rule.Os.Name == '%s', but is true on %s\n", windows.Os.Name, runtime.GOOS)
}
x86 := Rule{Action: ActionAllow}
x86.Os.Arch = "x86"
if ok, _ := x86.Check(); runtime.GOARCH != "386" && ok {
t.Fatalf("Rule.Os.Arch == '%s', but is true on %s\n", windows.Os.Arch, runtime.GOOS)
}
emptyAllow := Rule{Action: ActionAllow}
if ok, _ := emptyAllow.Check(); !ok {
t.Fatalf("An empty allow rule must return true\n")
}
disOsx := Rule{Action: ActionDisallow}
disOsx.Os.Name = "osx"
if ok, _ := disOsx.Check(); runtime.GOOS != "darwin" && !ok {
t.Fatalf("A disallow rule for OSX must return false, and true for other OSes\n")
}
}

View File

@ -0,0 +1,47 @@
package mcclprofile
import (
"encoding/json"
"os"
"path"
)
const ProfileFileName = "mccl_profile.json"
type Profile struct {
Username string `json:"username"`
Uuid string `json:"uuid"`
JavaXmx string `json:"java-Xmx"`
}
func (p *Profile) Store(rootDir string) error {
pf, err := os.OpenFile(path.Join(rootDir, ProfileFileName), os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return err
}
defer pf.Close()
je := json.NewEncoder(pf)
je.SetIndent("", "\t")
if err := je.Encode(p); err != nil {
return err
}
return nil
}
func Load(rootDir string) (*Profile, error) {
p := &Profile{}
pf, err := os.Open(path.Join(rootDir, ProfileFileName))
if err != nil {
return nil, err
}
defer pf.Close()
jd := json.NewDecoder(pf)
if err := jd.Decode(p); err != nil {
return nil, err
}
return p, nil
}

View File

@ -0,0 +1,74 @@
package version_manifest
import (
"encoding/json"
"fmt"
"time"
)
// VersionManifestUrl is a URL to a version manifest JSON file of version 2.
const VersionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
// VersionType is telling if it is a stable release or an intermedialry snapshot.
type VersionType string
const (
// VersionTypeSnapshot is an unstable intermediary version of a game.
VersionTypeSnapshot VersionType = "snapshot"
// VersionTypeRelease is a stable release version of a game.
VersionTypeRelease VersionType = "release"
)
// Version holds an information about a particular version.
type Version struct {
Id string `json:"id"`
Type VersionType `json:"type"`
Url string `json:"url"`
Time time.Time `json:"time"`
ReleaseTime time.Time `json:"releaseTime"`
Sha1 string `json:"sha1"`
}
// VersionManifest holds an array of all game versions, also a latest metadata
// that holds the latest release and snapshot.
type VersionManifest struct {
Latest struct {
Release string `json:"release"`
Snapshot string `json:"snapshot"`
} `json:"latest"`
Versions []Version `json:"versions"`
}
// New returns a new VersionManifest that holds info on every
// known Minecraft version.
func New(data []byte) (*VersionManifest, error) {
vm := &VersionManifest{}
if err := json.Unmarshal(data, vm); err != nil {
return nil, err
}
return vm, nil
}
// GetLatest returns a latest release or snapshot depending on a type provided.
func (vm *VersionManifest) GetLatest(typ VersionType) (Version, error) {
var version string
switch typ {
case VersionTypeSnapshot:
version = vm.Latest.Snapshot
case VersionTypeRelease:
version = vm.Latest.Release
default:
return Version{}, fmt.Errorf("version type %s doesn't exist", typ)
}
return vm.Get(version)
}
// Get returns a specific version of Minecraft.
func (vm *VersionManifest) Get(version string) (Version, error) {
for _, v := range vm.Versions {
if v.Id == version {
return v, nil
}
}
return Version{}, fmt.Errorf("version %s not found", version)
}

81
pkg/retriever/retriever.go Executable file
View File

@ -0,0 +1,81 @@
package retriever
import (
"fmt"
"runtime"
"sync"
)
// Retrievable must be implemented by any type used with Retriever.
type Retrievable interface {
// Retrieve is used just to retrieve some data from somewhere (usually from
// network) and return a slice of bytes or nil and an error. If you need to
// write the data retrieved use a WorkerFunc where you can call this method.
Retrieve() ([]byte, error)
}
// Retriever allows to retrieve a bunch of files in parallel taking care of
// errors and a done channel that returns an amount of bytes written.
type Retriever struct {
retrieveErrors chan error
retrieveDone chan int
}
// New returns a new Retriever with a base path set.
func New() *Retriever {
return &Retriever{
retrieveErrors: make(chan error, 1),
retrieveDone: make(chan int)}
}
// GetErrorChan returns an error channel.
//
// An error channel holds just one error and hangs after that. So it is considered
// as a "soft panic".
func (r *Retriever) GetErrorChan() chan error {
return r.retrieveErrors
}
// GetDoneChan returns a channel that is used to track a progress. Its value
// is an amount of bytes written for an item.
func (r *Retriever) GetDoneChan() chan int {
return r.retrieveDone
}
// WorkerFunc accepts a Retrievable item and a basePath where an item should be
// placed to. And it returns how much was written and an error if something
// went wrong.
type WorkerFunc func(item Retrievable, basePath string) (int, error)
// Run runs in parallel a number of CPU threads * 2 WorkerFunc functions.
// If an error occured it stops and returns that error.
func (r *Retriever) Run(items []Retrievable, worker WorkerFunc, basePath string) error {
var workersGroup sync.WaitGroup
retrieveLimiter := make(chan struct{}, runtime.NumCPU()*2)
for _, item := range items {
if len(r.retrieveErrors) > 0 {
workersGroup.Wait()
return <-r.retrieveErrors
}
retrieveLimiter <- struct{}{}
workersGroup.Add(1)
go func(item Retrievable) {
bytesLen, err := worker(item, basePath)
workersGroup.Done()
<-retrieveLimiter
if err != nil {
r.retrieveErrors <- fmt.Errorf("failed to retrieve: %s", err)
return
}
r.retrieveDone <- bytesLen
}(item)
}
workersGroup.Wait()
return nil
}
// Close the channels.
func (r *Retriever) Close() {
close(r.retrieveDone)
close(r.retrieveErrors)
}

View File

@ -0,0 +1,8 @@
package util
func LocateJavaHome(component string, majorVersion int) (string, error) {
if component == "jre-legacy" {
return "/usr/lib/jvm/java-8-openjdk/jre", nil
}
return "/usr/lib/jvm/default", nil
}

View File

@ -0,0 +1,68 @@
package util
import (
"fmt"
"strconv"
"strings"
"golang.org/x/sys/windows/registry"
)
const legacyJavaRegPath = "SOFTWARE\\JavaSoft\\Java Runtime Environment"
const jdkRegPath = "SOFTWARE\\JavaSoft\\JDK"
func LocateJavaHome(component string, majorVersion int) (string, error) {
if component == "jre-legacy" {
jre, err := registry.OpenKey(registry.LOCAL_MACHINE, legacyJavaRegPath, registry.QUERY_VALUE)
if err != nil {
return "", err
}
curVer, _, err := jre.GetStringValue("CurrentVersion")
if err != nil {
return "", nil
}
mVer, err := strconv.ParseInt(strings.Split(curVer, ".")[1], 10, 64)
if err != nil {
return "", nil
}
if mVer < int64(majorVersion) {
return "", fmt.Errorf("installed JRE version %s is older than allowed 1.%d", curVer, majorVersion)
}
jre, err = registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("%s\\%s", legacyJavaRegPath, curVer), registry.QUERY_VALUE)
if err != nil {
return "", err
}
javaHome, _, err := jre.GetStringValue("JavaHome")
if err != nil {
return "", err
}
return javaHome, nil
} else {
jdk, err := registry.OpenKey(registry.LOCAL_MACHINE, jdkRegPath, registry.QUERY_VALUE)
if err != nil {
return "", err
}
curVer, _, err := jdk.GetStringValue("CurrentVersion")
if err != nil {
return "", nil
}
mVer, err := strconv.ParseInt(strings.Split(curVer, ".")[0], 10, 64)
if err != nil {
return "", nil
}
if mVer < int64(majorVersion) {
return "", fmt.Errorf("installed JDK version %d is older than allowed %d", mVer, majorVersion)
}
jdk, err = registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("%s\\%s", jdkRegPath, curVer), registry.QUERY_VALUE)
if err != nil {
return "", err
}
javaHome, _, err := jdk.GetStringValue("JavaHome")
if err != nil {
return "", err
}
return javaHome, nil
}
return "", nil
}

View File

@ -0,0 +1,26 @@
package util
import (
"errors"
"os"
"runtime"
"strings"
)
func OSVersion() (string, error) {
switch osName := runtime.GOOS; osName {
case "linux":
// Implemented just in case for future. Guess if it will ever be needed
// then a kernel version will be used to compare against.
osRelease, err := os.ReadFile("/proc/sys/kernel/osrelease")
if err != nil {
return "", err
}
kVer := strings.Split(string(osRelease), "-")
if len(kVer) >= 1 {
return kVer[0], nil
}
return "", errors.New("malformed osrelease")
}
return "", errors.New("unknown OS")
}

View File

@ -0,0 +1,29 @@
package util
import (
"errors"
"fmt"
"runtime"
"golang.org/x/sys/windows/registry"
)
func OSVersion() (string, error) {
switch osName := runtime.GOOS; osName {
case "windows":
verKey, err := registry.OpenKey(registry.LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion", registry.QUERY_VALUE)
if err != nil {
return "", err
}
major, _, err := verKey.GetIntegerValue("CurrentMajorVersionNumber")
if err != nil {
return "", err
}
minor, _, err := verKey.GetIntegerValue("CurrentMinorVersionNumber")
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d", major, minor), nil
}
return "", errors.New("unknown OS")
}

145
pkg/util/util.go Executable file
View File

@ -0,0 +1,145 @@
package util
import (
"crypto/md5"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"hash"
"io"
"net/http"
"os"
"path"
"runtime"
"strings"
)
type File struct {
Id string `json:"id"`
Sha1 string `json:"sha1"`
Size int64 `json:"size"`
Url string `json:"url"`
Path string `json:"path"`
}
func (f File) Retrieve() ([]byte, error) {
return GetFromUrl(f.Url, f.Sha1, "sha1", f.Size)
}
func (f *File) GetName() string {
if f.Id == "" {
return f.Path
}
return f.Id
}
func GetFromUrl(url string, hash, hashType string, fSize int64) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code returned is %d", resp.StatusCode)
}
if fSize != -1 && resp.ContentLength != -1 && resp.ContentLength != fSize {
return nil, fmt.Errorf("response size mismatch. %d != %d", resp.ContentLength, fSize)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if fSize != -1 && len(data) != int(fSize) {
return nil, fmt.Errorf("body size mismatch. %d != %d", len(data), fSize)
}
if hash != "" {
if err := CheckHash(data, hash, hashType); err != nil {
return nil, err
}
}
return data, nil
}
func IsFileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func WriteFile(fPath string, data []byte) error {
if err := os.MkdirAll(path.Dir(fPath), 0777); err != nil {
return err
}
if err := os.WriteFile(fPath, data, 0666); err != nil {
return err
}
return nil
}
func ReadFile(fPath string) ([]byte, error) {
if !IsFileExist(fPath) {
return nil, fmt.Errorf("File %s not found", fPath)
}
data, err := os.ReadFile(fPath)
if err != nil {
return nil, err
}
return data, nil
}
// LoadOrDownloadFile will check if file exists and correct, will download it otherwise.
func LoadOrDownloadFile(fPath, url string, hash, hashType string, fSize int64) ([]byte, error) {
if IsFileExist(fPath) {
data, err := os.ReadFile(fPath)
if err != nil {
return nil, err
}
if hash != "" {
if err := CheckHash(data, hash, hashType); err != nil {
return nil, err
}
}
return data, nil
}
return GetFromUrl(url, hash, hashType, fSize)
}
func CheckHash(data []byte, fHash, hashType string) error {
var hasher hash.Hash
switch hashType {
case "sha1":
hasher = sha1.New()
case "md5":
hasher = md5.New()
default:
return fmt.Errorf("unsupported hash type %s", hashType)
}
if _, err := hasher.Write(data); err != nil {
return err
}
resultedHash := hasher.Sum(nil)
if (strings.Contains(fHash, "=") && base64.StdEncoding.EncodeToString(resultedHash) == fHash) ||
hex.EncodeToString(resultedHash) == fHash {
return nil
}
return fmt.Errorf("hash mismatch: %s != %s", hex.EncodeToString(resultedHash), fHash)
}
func PathSeparator() string {
if runtime.GOOS == "windows" {
return ";"
}
return ":"
}