package topdata import ( "encoding/json" "fmt" "os" "path/filepath" "sort" ) func importLegacyRuleset(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) { _ = legacyTLK legacyDir := filepath.Join(referenceBuilderDir, "data", "ruleset") if _, err := os.Stat(legacyDir); err != nil { if os.IsNotExist(err) { return 0, nil } return 0, err } targetDir := filepath.Join(dataDir, "ruleset") targetPath := filepath.Join(targetDir, "ruleset.json") if _, err := os.Stat(targetPath); err == nil { obj, err := loadJSONObject(targetPath) if err == nil && countLegacyTLKRefsInValue(obj) == 0 { return 0, nil } } baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json")) if err != nil { return 0, err } rows, err := mergeRulesetRows(baseObj, filepath.Join(legacyDir, "modules")) if err != nil { return 0, err } if err := os.MkdirAll(targetDir, 0o755); err != nil { return 0, err } if err := removePathIfExists(filepath.Join(targetDir, "base.json")); err != nil { return 0, err } if err := removePathIfExists(filepath.Join(targetDir, "lock.json")); err != nil { return 0, err } if err := removePathIfExists(filepath.Join(targetDir, "modules")); err != nil { return 0, err } out := map[string]any{ "output": "ruleset.2da", "columns": baseObj["columns"], "rows": rows, } raw, err := json.MarshalIndent(out, "", " ") if err != nil { return 0, err } raw = append(raw, '\n') if err := os.WriteFile(targetPath, raw, 0o644); err != nil { return 0, err } return 1, nil } func mergeRulesetRows(baseObj map[string]any, modulesDir string) ([]any, error) { rawRows, ok := baseObj["rows"].([]any) if !ok { return nil, nil } rows := make([]map[string]any, 0, len(rawRows)) byLabel := map[string]map[string]any{} for _, raw := range rawRows { row, ok := deepCopyValue(raw).(map[string]any) if !ok { continue } delete(row, "key") if rawID, ok := row["id"]; ok { id, err := asInt(rawID) if err != nil { return nil, err } row["id"] = id } if label, ok := row["Label"].(string); ok && label != "" && label != "****" { if _, exists := byLabel[label]; exists { return nil, fmt.Errorf("duplicate ruleset label %q", label) } byLabel[label] = row } rows = append(rows, row) } modulePaths, err := collectModulePaths(modulesDir) if err != nil { return nil, err } for _, path := range modulePaths { obj, err := loadJSONObject(path) if err != nil { return nil, err } overrideList, ok := obj["overrides"].([]any) if !ok { continue } for _, raw := range overrideList { override, ok := raw.(map[string]any) if !ok { continue } matchObj, ok := override["match"].(map[string]any) if !ok { return nil, fmt.Errorf("%s: ruleset override is missing match object", path) } rawLabel, ok := matchObj["Label"] if !ok { return nil, fmt.Errorf("%s: ruleset override match is missing Label", path) } labels, err := normalizeRulesetMatchLabels(rawLabel) if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } for _, label := range labels { row, ok := byLabel[label] if !ok { return nil, fmt.Errorf("%s: ruleset override target label %q was not found", path, label) } for key, value := range override { if key == "id" || key == "key" || key == "_tlk" || isMetadataField(key) || key == "match" { continue } row[key] = deepCopyValue(value) } } } } sort.Slice(rows, func(i, j int) bool { return rows[i]["id"].(int) < rows[j]["id"].(int) }) out := make([]any, 0, len(rows)) for _, row := range rows { out = append(out, row) } return out, nil } func normalizeRulesetMatchLabels(raw any) ([]string, error) { switch typed := raw.(type) { case string: return []string{typed}, nil case []any: out := make([]string, 0, len(typed)) for _, item := range typed { label, ok := item.(string) if !ok { return nil, fmt.Errorf("ruleset match.Label entries must be strings") } out = append(out, label) } return out, nil default: return nil, fmt.Errorf("ruleset match.Label must be a string or string array") } }