Files
ConfigKit/internal/engine/source_version.go

490 lines
15 KiB
Go

package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
type sourceVersionMode uint8
const (
sourceVersionModeDefault sourceVersionMode = iota
sourceVersionModeIfPresent
sourceVersionModeRequired
)
// SourceVersionModeFromFacade converts the root-owned public mode while
// keeping the engine's policy type private. Unknown values remain intact for
// pre-acquisition validation.
func SourceVersionModeFromFacade(value uint8) sourceVersionMode {
return sourceVersionMode(value)
}
type sourceVersionRole uint8
const (
sourceVersionMain sourceVersionRole = iota
sourceVersionContribution
)
type sourceVersionAncestor struct {
path Path
}
type sourceVersionEvidence struct {
sourceID string
path Path
resolvedPath Path
destination Path
projectedPaths []Path
version Version
location *Location
ancestors []sourceVersionAncestor
redacted bool
}
type sourceVersionState struct {
localRule *VersionRule
effectiveRule *VersionRule
mode sourceVersionMode
mainProcessing *sourceVersionProcessingResult
evidence []*sourceVersionEvidence
diagnostics []Diagnostic
}
func (state *sourceVersionState) evidenceSince(start int) []*sourceVersionEvidence {
if state == nil {
return nil
}
return state.evidence[start:]
}
func sourceInfosWithVersionEvidence(sources []SourceInfo, evidence []*sourceVersionEvidence, secrets map[string]bool, conservative bool) []SourceInfo {
result := cloneSourceInfos(sources)
if len(evidence) == 0 {
return result
}
sensitivity := newDiffSensitivity(secrets, nil)
sourceIndexes := make(map[string]int, len(result))
for index := range result {
if _, found := sourceIndexes[result[index].ID]; !found {
sourceIndexes[result[index].ID] = index
}
}
for _, item := range evidence {
if item == nil || item.sourceID == "" {
continue
}
if index, found := sourceIndexes[item.sourceID]; found {
result[index].VersionCheck = sourceVersionCheckForEvidence(item, sensitivity, conservative)
}
}
return result
}
func sourceVersionCheckForEvidence(evidence *sourceVersionEvidence, sensitivity *diffSensitivity, conservative bool) *SourceVersionCheck {
path := evidence.resolvedPath
if !path.valid() {
path = evidence.path
}
version := evidence.version
redacted := evidence.redacted
if conservative {
redacted = redacted || version.Text != ""
} else if sensitivity.isSensitiveAtOrAbove(evidence.sensitivityPaths()[0].String()) {
redacted = true
}
if redacted {
version.Text = ""
}
return &SourceVersionCheck{
Path: path.clone(), Version: version, Location: cloneLocation(evidence.location), Redacted: redacted,
}
}
// sensitivityPaths preserves source-local evidence while describing its assigned
// application coordinates. Before assembly, the original mount is the fallback.
func (evidence *sourceVersionEvidence) sensitivityPaths() []Path {
if evidence.projectedPaths != nil {
return evidence.projectedPaths
}
local := evidence.resolvedPath
if !local.valid() {
local = evidence.path
}
paths := make([]Path, 0, len(evidence.ancestors)+1)
paths = append(paths, appendDestination(evidence.destination, local))
for _, ancestor := range evidence.ancestors {
paths = append(paths, appendDestination(evidence.destination, ancestor.path))
}
return paths
}
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) {
if state == nil || state.mode == sourceVersionModeDefault {
return sourceVersionProcessingResult{}, nil
}
return state.processWithDirectiveContext(root, role, destination, sourceID, placement, rootDirectiveContextImportsOnly)
}
func (state *sourceVersionState) processWithDirectiveContext(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode, directiveContext rootDirectiveContext) (sourceVersionProcessingResult, []Diagnostic) {
if state == nil {
return sourceVersionProcessingResult{}, nil
}
projectedDestination := destination.clone()
if placement == placementModeSequenceItem {
projectedDestination = projectedDestination.Index(0)
}
result := processSourceVersionWithDirectiveContext(root, state.localRule, role, projectedDestination, state.mode, directiveContext)
if result.evidence != nil {
result.evidence.sourceID = sourceID
state.evidence = append(state.evidence, result.evidence)
}
var failures []Diagnostic
for _, diagnostic := range result.diagnostics {
if diagnostic.Severity == SeverityWarning {
state.diagnostics = append(state.diagnostics, diagnostic)
} else if diagnostic.Severity == SeverityError {
failures = append(failures, diagnostic)
}
}
return result, failures
}
type sourceVersionProcessingResult struct {
payload *document.Node
imports []importDeclarationSpec
sequences []sequenceDeclarationSpec
directiveContext rootDirectiveContext
evidence *sourceVersionEvidence
diagnostics []Diagnostic
}
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
return effectiveSourceVersionRuleForPlacement(local, destination, mode, placementModeMapping)
}
func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path, mode sourceVersionMode, placement placementMode) *VersionRule {
if local == nil {
return nil
}
if mode == sourceVersionModeDefault {
return local
}
effective := *local
prefix := destination.clone()
if placement == placementModeSequenceItem {
prefix = prefix.Index(0)
}
effective.Path = Path{
segments: append(clonePathSegments(prefix.segments, len(local.Path.segments)), local.Path.segments...),
invalid: prefix.invalid || local.Path.invalid,
}
return &effective
}
type sourceVersionPathAbsence uint8
const (
sourceVersionPathPresent sourceVersionPathAbsence = iota
sourceVersionPathInvalid
sourceVersionMemberMissing
sourceVersionIndexUnavailable
sourceVersionPathObstructed
)
type sourceVersionPath struct {
node *document.Node
present bool
absence sourceVersionPathAbsence
nearest document.Location
resolvedPath Path
ancestors []sourceVersionAncestor
}
func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult {
return processSourceVersionWithDirectiveContext(root, rule, role, destination, mode, rootDirectiveContextImportsOnly)
}
func processSourceVersionWithDirectiveContext(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode, directiveContext rootDirectiveContext) sourceVersionProcessingResult {
if role != sourceVersionMain && role != sourceVersionContribution {
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}}
}
if mode != sourceVersionModeDefault && mode != sourceVersionModeIfPresent && mode != sourceVersionModeRequired {
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version mode is invalid")}}
}
if mode != sourceVersionModeDefault && (rule == nil || rule.Check == nil || !rule.Path.valid()) {
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version checking requires a valid version rule and checker")}}
}
if root == nil {
if role == sourceVersionMain && mode != sourceVersionModeDefault {
return sourceVersionProcessingResult{diagnostics: canonicalDiagnostics(reportVersion(rule, Version{}, nil))}
}
return sourceVersionProcessingResult{}
}
extracted, diagnostics := extractRootDirectives(root, directiveContext)
if len(diagnostics) != 0 {
return sourceVersionProcessingResult{diagnostics: diagnostics}
}
result := sourceVersionProcessingResult{
payload: extracted.body, imports: extracted.imports, directiveContext: directiveContext,
sequences: cloneSequenceDeclarations(extracted.sequences),
}
if mode == sourceVersionModeDefault {
return result
}
resolved := resolveSourceVersionPath(root, rule.Path)
evidence := &sourceVersionEvidence{
path: rule.Path.clone(), resolvedPath: resolved.resolvedPath,
destination: destination.clone(), ancestors: resolved.ancestors,
}
location := resolved.nearest
if resolved.present {
location = resolved.node.Location
}
if locationKnown(location) {
public := publicLocation(location)
evidence.location = &public
}
result.evidence = evidence
interpreted := interpretVersion(rule, root)
evidence.version = interpreted.version
if sourceVersionHeaderControlOverlap(root, resolved) {
evidence.redactText()
result.payload = nil
result.diagnostics = []Diagnostic{sourceVersionOverlapDiagnostic(rule, evidence)}
return result
}
if hasErrors(interpreted.diagnostics) {
evidence.redactText()
result.payload = nil
result.diagnostics = canonicalDiagnostics(interpreted.diagnostics)
return result
}
if !interpreted.version.Present {
if mode == sourceVersionModeRequired {
evidence.redactText()
result.payload = nil
result.diagnostics = []Diagnostic{sourceVersionMissingDiagnostic(rule, evidence)}
return result
}
if role == sourceVersionContribution {
return result
}
}
result.diagnostics = canonicalDiagnostics(reportVersion(rule, interpreted.version, root))
if hasErrors(result.diagnostics) {
evidence.redactText()
result.payload = nil
return result
}
if role == sourceVersionContribution && interpreted.version.Present {
if !removeSourceVersionNode(result.payload, evidence.resolvedPath) {
evidence.redactText()
result.payload = nil
result.diagnostics = []Diagnostic{sourceVersionOptionsDiagnostic("selected source version could not be removed from its owned payload")}
return result
}
}
return result
}
func sourceVersionHeaderControlOverlap(root *document.Node, resolved sourceVersionPath) bool {
return resolved.present && sourceVersionControlOverlap(root, resolved.resolvedPath)
}
func sourceVersionOptionsDiagnostic(message string) Diagnostic {
return Diagnostic{Code: CodeOptionsInvalid, Severity: SeverityError, Message: message}
}
func sourceVersionMissingDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
return Diagnostic{
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
Message: "required schema version is missing", Primary: cloneLocation(evidence.location),
}
}
func sourceVersionOverlapDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
return Diagnostic{
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
Message: "schema version path overlaps a structural directive", Primary: cloneLocation(evidence.location),
}
}
func (evidence *sourceVersionEvidence) redactText() {
if evidence == nil {
return
}
evidence.version.Text = ""
evidence.redacted = true
}
func resolveSourceVersionPath(root *document.Node, path Path) sourceVersionPath {
// Refine only segments whose container shape is known, keeping the full
// requested suffix when lookup stops before the declaration.
resolved := sourceVersionPath{absence: sourceVersionPathInvalid, resolvedPath: path.clone()}
if root == nil || !path.valid() {
return resolved
}
current := root
resolved.nearest = current.Location
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{})
for position, segment := range path.segments {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
resolved.absence = sourceVersionPathObstructed
return resolved
}
resolved.resolvedPath.segments[position] = pathSegment{kind: pathSegmentKey, key: segment.key}
found := false
for _, entry := range current.Mapping {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
resolved.absence = sourceVersionMemberMissing
return resolved
}
case document.Sequence:
index, ok := schemaIndex(segment)
if !ok {
resolved.absence = sourceVersionIndexUnavailable
return resolved
}
resolved.resolvedPath.segments[position] = pathSegment{kind: pathSegmentIndex, index: index}
if index >= len(current.Sequence) {
resolved.absence = sourceVersionIndexUnavailable
return resolved
}
current = current.Sequence[index]
default:
resolved.absence = sourceVersionPathObstructed
return resolved
}
resolved.nearest = current.Location
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{
path: Path{segments: clonePathSegments(resolved.resolvedPath.segments[:position+1], 0)},
})
}
resolved.node = current
resolved.present = true
resolved.absence = sourceVersionPathPresent
return resolved
}
func sourceVersionControlOverlap(root *document.Node, path Path) bool {
if root == nil || !path.valid() {
return false
}
if root.Kind == document.Mapping {
for _, entry := range root.Mapping {
if entry.Key == directiveKey && pathHasPrefix(path, Keys(directiveKey)) {
return true
}
}
}
return sourceVersionInheritanceOverlap(root, Path{}, path)
}
func sourceVersionInheritanceOverlap(node *document.Node, current, path Path) bool {
if node == nil {
return false
}
if node.Kind == document.Mapping {
if _, found := inheritanceDirective(node); found {
controlPath := current.Key(inheritanceKey)
if pathHasPrefix(path, controlPath) {
return true
}
}
for _, entry := range node.Mapping {
if entry.Key == inheritanceKey {
continue
}
if sourceVersionInheritanceOverlap(entry.Value, current.Key(entry.Key), path) {
return true
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if sourceVersionInheritanceOverlap(child, current.Index(index), path) {
return true
}
}
}
return false
}
func pathHasPrefix(path, prefix Path) bool {
if !path.valid() || !prefix.valid() || len(prefix.segments) > len(path.segments) {
return false
}
for index, segment := range prefix.segments {
candidate := path.segments[index]
if candidate.kind != segment.kind || candidate.key != segment.key || candidate.index != segment.index {
return false
}
}
return true
}
func removeSourceVersionNode(root *document.Node, path Path) bool {
if root == nil || !path.valid() || len(path.segments) == 0 {
return false
}
current := root
for _, segment := range path.segments[:len(path.segments)-1] {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return false
}
found := false
for _, entry := range current.Mapping {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
return false
}
case document.Sequence:
index, ok := schemaIndex(segment)
if !ok || index >= len(current.Sequence) {
return false
}
current = current.Sequence[index]
default:
return false
}
}
last := path.segments[len(path.segments)-1]
switch current.Kind {
case document.Mapping:
if last.kind == pathSegmentIndex {
return false
}
for index, entry := range current.Mapping {
if entry.Key == last.key {
current.Mapping = append(current.Mapping[:index], current.Mapping[index+1:]...)
return true
}
}
case document.Sequence:
index, ok := schemaIndex(last)
if !ok || index >= len(current.Sequence) {
return false
}
current.Sequence = append(current.Sequence[:index], current.Sequence[index+1:]...)
return true
}
return false
}