package app import ( "time" transportcfgpkg "selective-vpn-api/app/transportcfg" ) type singBoxProfileEvalResult struct { Valid bool Config map[string]any Digest string Diff SingBoxProfileRenderDiff Changed bool Errors []SingBoxProfileIssue Warnings []SingBoxProfileIssue } func evaluateSingBoxProfile(profile SingBoxProfile, checkBinary bool) singBoxProfileEvalResult { deps := transportcfgpkg.SingBoxProfileEvalDeps{ NormalizeMode: func(mode string) (string, bool) { nm, ok := normalizeSingBoxProfileMode(SingBoxProfileMode(mode)) return string(nm), ok }, CloneMapDeep: cloneMapDeep, AsString: asString, ProtocolSupported: transportcfgpkg.SingBoxTypedProtocolSupported, ParsePort: transportcfgpkg.ParsePort, } in := transportcfgpkg.SingBoxProfileInput{ ID: profile.ID, Mode: string(profile.Mode), Protocol: profile.Protocol, RawConfig: profile.RawConfig, Typed: profile.Typed, } pkgErrs, pkgWarns := transportcfgpkg.ValidateSingBoxProfile(in, deps) errs := singBoxIssuesFromPkg(pkgErrs) warns := singBoxIssuesFromPkg(pkgWarns) rendered, renderErr := transportcfgpkg.RenderSingBoxProfileConfig(in, deps) if renderErr == nil { rendered = transportcfgpkg.NormalizeSingBoxRenderedConfig(rendered, asString) } if renderErr != nil { errs = append(errs, SingBoxProfileIssue{ Field: "profile", Severity: "error", Code: "SINGBOX_RENDER_INVALID", Message: renderErr.Error(), }) } if checkBinary && renderErr == nil { if issue := validateSingBoxWithBinary(rendered); issue != nil { if issue.Severity == "warning" { warns = append(warns, *issue) } else { errs = append(errs, *issue) } } } valid := len(errs) == 0 diff := SingBoxProfileRenderDiff{} digest := "" changed := false if rendered != nil { digest = transportcfgpkg.DigestJSONMap(rendered) prev := transportcfgpkg.ReadJSONMapFile(singBoxRenderedPath(profile.ID)) pkgDiff, cfgChanged := transportcfgpkg.DiffConfigMaps(prev, rendered) diff = SingBoxProfileRenderDiff{ Added: pkgDiff.Added, Removed: pkgDiff.Removed, Changed: pkgDiff.Changed, } changed = cfgChanged } return singBoxProfileEvalResult{ Valid: valid, Config: rendered, Digest: digest, Diff: diff, Changed: changed, Errors: errs, Warnings: warns, } } func singBoxIssuesFromPkg(in []transportcfgpkg.SingBoxIssue) []SingBoxProfileIssue { if len(in) == 0 { return nil } out := make([]SingBoxProfileIssue, 0, len(in)) for _, it := range in { out = append(out, SingBoxProfileIssue{ Field: it.Field, Severity: it.Severity, Code: it.Code, Message: it.Message, }) } return out } func validateSingBoxWithBinary(config map[string]any) *SingBoxProfileIssue { issue := transportcfgpkg.ValidateRenderedConfigWithBinary( config, runCommandTimeout, 5*time.Second, "/usr/local/bin/sing-box", "/usr/bin/sing-box", "sing-box", ) if issue == nil { return nil } return &SingBoxProfileIssue{ Field: issue.Field, Severity: issue.Severity, Code: issue.Code, Message: issue.Message, } } func writeSingBoxRenderedConfig(profileID string, config map[string]any) (string, error) { path := singBoxRenderedPath(profileID) if err := transportcfgpkg.WriteJSONConfigFile(path, config); err != nil { return path, err } return path, nil } func singBoxRenderedPath(profileID string) string { return transportcfgpkg.ProfileConfigPath(singBoxRenderedRootDir, profileID, sanitizeID) } func defaultSingBoxAppliedConfigPath(profileID string) string { return transportcfgpkg.ProfileConfigPath(singBoxAppliedRootDir, profileID, sanitizeID) }