I think it's time to init a repo already. xD
This commit is contained in:
commit
d60493e1b1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
bin/
|
32
Makefile
Executable file
32
Makefile
Executable 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
26
build/PKGBUILD
Normal 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
5
cmd/mccl/commands/command.go
Executable file
@ -0,0 +1,5 @@
|
||||
package commands
|
||||
|
||||
type Command interface {
|
||||
Run() error
|
||||
}
|
325
cmd/mccl/commands/install_command.go
Executable file
325
cmd/mccl/commands/install_command.go
Executable 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
|
||||
}
|
33
cmd/mccl/commands/list_command.go
Executable file
33
cmd/mccl/commands/list_command.go
Executable 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
163
cmd/mccl/commands/run_command.go
Executable 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
232
cmd/mccl/main.go
Executable 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
5
go.mod
Executable file
@ -0,0 +1,5 @@
|
||||
module mccl
|
||||
|
||||
go 1.17
|
||||
|
||||
require golang.org/x/sys v0.13.0
|
2
go.sum
Executable file
2
go.sum
Executable 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
124
internal/assets/assets.go
Executable 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
60
internal/manifest/argument.go
Executable 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
|
||||
}
|
16
internal/manifest/argument_test.go
Executable file
16
internal/manifest/argument_test.go
Executable 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
136
internal/manifest/library.go
Executable 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
200
internal/manifest/manifest.go
Executable 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
122
internal/manifest/rule.go
Executable 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
49
internal/manifest/rule_test.go
Executable 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")
|
||||
}
|
||||
}
|
47
internal/mccl_profile/profile.go
Normal file
47
internal/mccl_profile/profile.go
Normal 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
|
||||
}
|
74
internal/version_manifest/version_manifest.go
Executable file
74
internal/version_manifest/version_manifest.go
Executable 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
81
pkg/retriever/retriever.go
Executable 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)
|
||||
}
|
8
pkg/util/locatejavahome_unix.go
Normal file
8
pkg/util/locatejavahome_unix.go
Normal 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
|
||||
}
|
68
pkg/util/locatejavahome_windows.go
Normal file
68
pkg/util/locatejavahome_windows.go
Normal 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
|
||||
}
|
26
pkg/util/osversion_unix.go
Normal file
26
pkg/util/osversion_unix.go
Normal 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")
|
||||
}
|
29
pkg/util/osversion_windows.go
Normal file
29
pkg/util/osversion_windows.go
Normal 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
145
pkg/util/util.go
Executable 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 ":"
|
||||
}
|
Loading…
Reference in New Issue
Block a user