237 lines
5.2 KiB
Go
237 lines
5.2 KiB
Go
package mods
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
maxMetadataEntrySize = 2 * 1024 * 1024
|
|
maxCompressionRatio = 200
|
|
)
|
|
|
|
type jarMetadata struct {
|
|
ID string
|
|
Name string
|
|
Version string
|
|
HasFabricMeta bool
|
|
HasForgeMeta bool
|
|
HasPluginMeta bool
|
|
MinecraftVersion string
|
|
}
|
|
|
|
func parseJarMetadata(path string) (jarMetadata, error) {
|
|
r, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
return jarMetadata{}, err
|
|
}
|
|
defer r.Close()
|
|
|
|
var out jarMetadata
|
|
for _, f := range r.File {
|
|
name := strings.ToLower(f.Name)
|
|
|
|
switch name {
|
|
case "fabric.mod.json":
|
|
out.HasFabricMeta = true
|
|
b, err := readZipEntryLimited(f, maxMetadataEntrySize)
|
|
if err == nil {
|
|
mergeFabricMetadata(&out, b)
|
|
}
|
|
case "meta-inf/mods.toml":
|
|
out.HasForgeMeta = true
|
|
b, err := readZipEntryLimited(f, maxMetadataEntrySize)
|
|
if err == nil {
|
|
mergeModsTOMLMetadata(&out, b)
|
|
}
|
|
case "mcmod.info":
|
|
b, err := readZipEntryLimited(f, maxMetadataEntrySize)
|
|
if err == nil {
|
|
mergeMCModInfoMetadata(&out, b)
|
|
}
|
|
case "plugin.yml":
|
|
out.HasPluginMeta = true
|
|
b, err := readZipEntryLimited(f, maxMetadataEntrySize)
|
|
if err == nil {
|
|
mergePluginYAMLMetadata(&out, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
if out.ID == "" && out.Name == "" && out.Version == "" && !out.HasFabricMeta && !out.HasForgeMeta && !out.HasPluginMeta {
|
|
return out, errors.New("no metadata found")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func readZipEntryLimited(f *zip.File, limit int64) ([]byte, error) {
|
|
if f.UncompressedSize64 > uint64(limit) {
|
|
return nil, fmt.Errorf("zip entry too large: %s", f.Name)
|
|
}
|
|
if f.CompressedSize64 > 0 && f.UncompressedSize64/f.CompressedSize64 > maxCompressionRatio {
|
|
return nil, fmt.Errorf("zip entry compression ratio too high: %s", f.Name)
|
|
}
|
|
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
|
|
lr := io.LimitReader(rc, limit+1)
|
|
b, err := io.ReadAll(lr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(b)) > limit {
|
|
return nil, fmt.Errorf("zip entry too large after read: %s", f.Name)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func mergeFabricMetadata(out *jarMetadata, b []byte) {
|
|
var payload struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Depends interface{} `json:"depends"`
|
|
}
|
|
if err := json.Unmarshal(b, &payload); err != nil {
|
|
return
|
|
}
|
|
if out.ID == "" {
|
|
out.ID = strings.TrimSpace(payload.ID)
|
|
}
|
|
if out.Name == "" {
|
|
out.Name = strings.TrimSpace(payload.Name)
|
|
}
|
|
if out.Version == "" {
|
|
out.Version = strings.TrimSpace(payload.Version)
|
|
}
|
|
|
|
switch d := payload.Depends.(type) {
|
|
case map[string]any:
|
|
if v, ok := d["minecraft"]; ok {
|
|
out.MinecraftVersion = strings.TrimSpace(fmt.Sprint(v))
|
|
}
|
|
}
|
|
}
|
|
|
|
func mergeModsTOMLMetadata(out *jarMetadata, b []byte) {
|
|
lines := strings.Split(string(b), "\n")
|
|
for _, raw := range lines {
|
|
line := strings.TrimSpace(raw)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, val, ok := splitKV(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch key {
|
|
case "modId":
|
|
if out.ID == "" {
|
|
out.ID = val
|
|
}
|
|
case "displayName":
|
|
if out.Name == "" {
|
|
out.Name = val
|
|
}
|
|
case "version":
|
|
if out.Version == "" {
|
|
out.Version = val
|
|
}
|
|
case "loaderVersion":
|
|
if out.MinecraftVersion == "" {
|
|
out.MinecraftVersion = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func mergeMCModInfoMetadata(out *jarMetadata, b []byte) {
|
|
var arr []map[string]any
|
|
if err := json.Unmarshal(b, &arr); err == nil && len(arr) > 0 {
|
|
mergeModInfoMap(out, arr[0])
|
|
return
|
|
}
|
|
var obj map[string]any
|
|
if err := json.Unmarshal(b, &obj); err == nil {
|
|
mergeModInfoMap(out, obj)
|
|
}
|
|
}
|
|
|
|
func mergeModInfoMap(out *jarMetadata, obj map[string]any) {
|
|
if out.ID == "" {
|
|
out.ID = strings.TrimSpace(fmt.Sprint(obj["modid"]))
|
|
}
|
|
if out.Name == "" {
|
|
out.Name = strings.TrimSpace(fmt.Sprint(obj["name"]))
|
|
}
|
|
if out.Version == "" {
|
|
out.Version = strings.TrimSpace(fmt.Sprint(obj["version"]))
|
|
}
|
|
if out.MinecraftVersion == "" {
|
|
out.MinecraftVersion = strings.TrimSpace(fmt.Sprint(obj["mcversion"]))
|
|
}
|
|
}
|
|
|
|
func mergePluginYAMLMetadata(out *jarMetadata, b []byte) {
|
|
lines := strings.Split(string(b), "\n")
|
|
for _, raw := range lines {
|
|
line := strings.TrimSpace(raw)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, val, ok := splitKV(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch strings.ToLower(key) {
|
|
case "name":
|
|
if out.Name == "" {
|
|
out.Name = val
|
|
}
|
|
case "version":
|
|
if out.Version == "" {
|
|
out.Version = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func splitKV(line string) (string, string, bool) {
|
|
sep := "="
|
|
idx := strings.Index(line, sep)
|
|
if idx == -1 {
|
|
sep = ":"
|
|
idx = strings.Index(line, sep)
|
|
if idx == -1 {
|
|
return "", "", false
|
|
}
|
|
}
|
|
k := strings.TrimSpace(line[:idx])
|
|
v := strings.TrimSpace(line[idx+1:])
|
|
v = strings.Trim(v, `"'`)
|
|
if k == "" || v == "" {
|
|
return "", "", false
|
|
}
|
|
return k, v, true
|
|
}
|
|
|
|
func fallbackNameFromFilename(filename string) (id string, name string) {
|
|
base := strings.TrimSuffix(filename, filepath.Ext(filename))
|
|
base = strings.TrimSuffix(base, ".jar")
|
|
base = strings.TrimSuffix(base, ".disabled")
|
|
base = strings.TrimSpace(base)
|
|
if base == "" {
|
|
base = "unknown"
|
|
}
|
|
return sanitizeID(base), base
|
|
}
|