zlh-agent/internal/mods/metadata.go
2026-03-07 20:59:27 +00:00

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
}