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 }