package core import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" "github.com/Masterminds/semver/v3" "github.com/spf13/viper" ) // Pack stores the modpack metadata, usually in pack.toml type Pack struct { Name string `toml:"name"` Author string `toml:"author,omitempty"` Version string `toml:"version,omitempty"` Description string `toml:"description,omitempty"` PackFormat string `toml:"pack-format"` Index struct { // Path is stored in forward slash format relative to pack.toml File string `toml:"file"` HashFormat string `toml:"hash-format"` Hash string `toml:"hash,omitempty"` } `toml:"index"` Versions map[string]string `toml:"versions"` Export map[string]map[string]interface{} `toml:"export"` Options map[string]interface{} `toml:"options"` } const CurrentPackFormat = "packwiz:1.0.0" var PackFormatConstraintAccepted = mustParseConstraint("~1") var PackFormatConstraintSuggestUpgrade = mustParseConstraint("~1.0") func mustParseConstraint(s string) *semver.Constraints { c, err := semver.NewConstraint(s) if err != nil { panic(err) } return c } // LoadPack loads the modpack metadata to a Pack struct func LoadPack() (Pack, error) { var modpack Pack if _, err := toml.DecodeFile(viper.GetString("pack-file"), &modpack); err != nil { return Pack{}, err } // Check pack-format if len(modpack.PackFormat) == 0 { fmt.Println("Modpack manifest has no pack-format field; assuming packwiz:1.0.0") modpack.PackFormat = "packwiz:1.0.0" } if !strings.HasPrefix(modpack.PackFormat, "packwiz:") { return Pack{}, errors.New("pack-format field does not indicate a valid packwiz pack") } ver, err := semver.StrictNewVersion(strings.TrimPrefix(modpack.PackFormat, "packwiz:")) if err != nil { return Pack{}, fmt.Errorf("pack-format field is not valid semver: %w", err) } if !PackFormatConstraintAccepted.Check(ver) { return Pack{}, errors.New("the modpack is incompatible with this version of packwiz; please update") } if !PackFormatConstraintSuggestUpgrade.Check(ver) { fmt.Println("Modpack has a newer feature number than is supported by this version of packwiz. Update to the latest version of packwiz for new features and bugfixes!") } // TODO: suggest migration if necessary (primarily for 2.0.0) // Read options into viper if modpack.Options != nil { err := viper.MergeConfigMap(modpack.Options) if err != nil { return Pack{}, err } } if len(modpack.Index.File) == 0 { modpack.Index.File = "index.toml" } return modpack, nil } // LoadIndex attempts to load the index file of this modpack func (pack Pack) LoadIndex() (Index, error) { if filepath.IsAbs(pack.Index.File) { return LoadIndex(pack.Index.File) } fileNative := filepath.FromSlash(pack.Index.File) return LoadIndex(filepath.Join(filepath.Dir(viper.GetString("pack-file")), fileNative)) } // UpdateIndexHash recalculates the hash of the index file of this modpack func (pack *Pack) UpdateIndexHash() error { if viper.GetBool("no-internal-hashes") { pack.Index.HashFormat = "sha256" pack.Index.Hash = "" return nil } fileNative := filepath.FromSlash(pack.Index.File) indexFile := filepath.Join(filepath.Dir(viper.GetString("pack-file")), fileNative) if filepath.IsAbs(pack.Index.File) { indexFile = pack.Index.File } f, err := os.Open(indexFile) if err != nil { return err } // Hash usage strategy (may change): // Just use SHA256, overwrite existing hash regardless of what it is // May update later to continue using the same hash that was already being used h, stringer, err := GetHashImpl("sha256") if err != nil { _ = f.Close() return err } if _, err := io.Copy(h, f); err != nil { _ = f.Close() return err } hashString := stringer.HashToString(h.Sum(nil)) pack.Index.HashFormat = "sha256" pack.Index.Hash = hashString return f.Close() } // Write saves the pack file func (pack Pack) Write() error { f, err := os.Create(viper.GetString("pack-file")) if err != nil { return err } enc := toml.NewEncoder(f) // Disable indentation enc.Indent = "" err = enc.Encode(pack) if err != nil { _ = f.Close() return err } return f.Close() } // GetMCVersion gets the version of Minecraft this pack uses, if it has been correctly specified func (pack Pack) GetMCVersion() (string, error) { mcVersion, ok := pack.Versions["minecraft"] if !ok { return "", errors.New("no minecraft version specified in modpack") } return mcVersion, nil } func (pack Pack) GetPackName() string { if pack.Name == "" { return "export" } else if pack.Version == "" { return pack.Name } else { return pack.Name + "-" + pack.Version } }