package validator import ( "fmt" "strings" "browser.local/platform/domain" ) const ( MaxMetricSamplesPerQuery = 500 MaxBackupRecordsPerQuery = 200 ) func ValidateMetricSample(sample domain.MetricSample) error { var violations []string violations = appendRequired(violations, "id", sample.ID) violations = appendRequired(violations, "serverInstanceId", sample.ServerInstanceID) violations = appendRequired(violations, "source", sample.Source) if sample.CollectedAt.IsZero() { violations = append(violations, "collectedAt is required") } for name, value := range map[string]*float64{"tps": sample.TPS, "latencyMs": sample.LatencyMS, "cpuPercent": sample.CPUPercent, "memoryPercent": sample.MemoryPercent, "diskPercent": sample.DiskPercent} { if value != nil && (value == nil || *value < 0) { violations = append(violations, fmt.Sprintf("%s must not be negative", name)) } } return finish(violations) } func ValidateMetricSampleFilter(filter domain.MetricSampleFilter) error { var violations []string if filter.Limit < 0 || filter.Limit > MaxMetricSamplesPerQuery { violations = append(violations, fmt.Sprintf("limit must be between 0 and %d", MaxMetricSamplesPerQuery)) } if filter.Before.Before(filter.After) { violations = append(violations, "before must not precede after") } return finish(violations) } func ValidateBackupRecord(record domain.BackupRecord) error { var violations []string violations = appendRequired(violations, "id", record.ID) violations = appendRequired(violations, "serverInstanceId", record.ServerInstanceID) violations = appendRequired(violations, "artifactId", record.ArtifactID) violations = appendRequired(violations, "checksum", record.Checksum) if record.SizeBytes <= 0 { violations = append(violations, "sizeBytes must be positive") } if record.State != domain.BackupStatePending && record.State != domain.BackupStateAvailable && record.State != domain.BackupStateFailed && record.State != domain.BackupStateExpired { violations = append(violations, "state is invalid") } if len(record.RecoveryStatus) > 256 { violations = append(violations, "recoveryStatus is too long") } return finish(violations) } func ValidateBackupFilter(filter domain.BackupFilter) error { if filter.State != "" && filter.State != domain.BackupStatePending && filter.State != domain.BackupStateAvailable && filter.State != domain.BackupStateFailed && filter.State != domain.BackupStateExpired { return ValidationError{Violations: []string{"state is invalid"}} } return nil } func ValidateRemoteAdapterDeclaration(declaration domain.RemoteAdapterDeclaration) error { var violations []string violations = appendRequired(violations, "key", declaration.Key) violations = appendRequired(violations, "kind", string(declaration.Kind)) if len(declaration.TargetKeys) == 0 { violations = append(violations, "targetKeys must not be empty") } if declaration.TimeoutSeconds <= 0 || declaration.TimeoutSeconds > 300 { violations = append(violations, "timeoutSeconds must be between 1 and 300") } if declaration.MaxAttempts <= 0 || declaration.MaxAttempts > 5 { violations = append(violations, "maxAttempts must be between 1 and 5") } return finish(violations) } func ValidateRemoteAdapterRequest(request domain.RemoteAdapterRequest) error { var violations []string violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "declarationKey", request.DeclarationKey) violations = appendRequired(violations, "targetKey", request.TargetKey) violations = appendRequired(violations, "capability", request.Capability) violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey) if request.TimeoutSeconds < 0 || request.TimeoutSeconds > 300 { violations = append(violations, "timeoutSeconds must be between 0 and 300") } if request.MaxAttempts < 0 || request.MaxAttempts > 5 { violations = append(violations, "maxAttempts must be between 0 and 5") } if strings.ContainsAny(request.TargetKey, "\\\n\r") || strings.Contains(request.TargetKey, "://") || strings.ContainsAny(request.TargetKey, " ") { violations = append(violations, "targetKey must be a logical key") } if request.InputRef != "" && !validScopedInputRef(request.InputRef) { violations = append(violations, "inputRef is not allowed") } violations = append(violations, validateRemoteAdapterInputs("inputs", request.Inputs)...) return finish(violations) } func validateRemoteAdapterInputs(field string, inputs map[string]string) []string { if len(inputs) > 32 { return []string{field + " has too many values"} } var violations []string for key, value := range inputs { if !clientManagerIdentifierPattern.MatchString(key) || unsafeGameClientBridgePayloadKey(key) { violations = append(violations, field+" key is invalid or unsafe") } if len([]rune(value)) > 2048 { violations = append(violations, field+"."+key+" is too long") } for _, reason := range unsafePluginStringReasons(value) { violations = append(violations, field+"."+key+": "+reason) } } return violations }