434 lines
13 KiB
Go
434 lines
13 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
|
|
kind document.Kind
|
|
}
|
|
|
|
type sourceVersionEvidence struct {
|
|
sourceID string
|
|
path Path
|
|
resolvedPath Path
|
|
destination Path
|
|
version Version
|
|
location *Location
|
|
ancestors []sourceVersionAncestor
|
|
absence sourceVersionPathAbsence
|
|
checked bool
|
|
redacted bool
|
|
}
|
|
|
|
type sourceVersionState struct {
|
|
localRule *VersionRule
|
|
effectiveRule *VersionRule
|
|
mode sourceVersionMode
|
|
mainProcessing *sourceVersionProcessingResult
|
|
evidence []*sourceVersionEvidence
|
|
diagnostics []Diagnostic
|
|
}
|
|
|
|
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)
|
|
for _, item := range evidence {
|
|
if item == nil || item.sourceID == "" {
|
|
continue
|
|
}
|
|
for index := range result {
|
|
if result[index].ID == item.sourceID {
|
|
result[index].VersionCheck = sourceVersionCheckForEvidence(item, sensitivity, conservative)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
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(appendDestination(evidence.destination, path).String()) {
|
|
redacted = true
|
|
}
|
|
if redacted {
|
|
version.Text = ""
|
|
}
|
|
return &SourceVersionCheck{
|
|
Path: path.clone(), Version: version, Location: cloneLocation(evidence.location), Redacted: redacted,
|
|
}
|
|
}
|
|
|
|
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string) (sourceVersionProcessingResult, []Diagnostic) {
|
|
if state == nil || state.mode == sourceVersionModeDefault {
|
|
return sourceVersionProcessingResult{}, nil
|
|
}
|
|
result := processSourceVersion(root, state.localRule, role, destination, state.mode)
|
|
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 []importDeclaration
|
|
evidence *sourceVersionEvidence
|
|
diagnostics []Diagnostic
|
|
}
|
|
|
|
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
|
|
if local == nil {
|
|
return nil
|
|
}
|
|
if mode == sourceVersionModeDefault {
|
|
return local
|
|
}
|
|
effective := *local
|
|
effective.Path = Path{
|
|
segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...),
|
|
invalid: destination.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 {
|
|
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{}
|
|
}
|
|
|
|
payload, imports, diagnostics := splitImportDirectives(root)
|
|
if len(diagnostics) != 0 {
|
|
return sourceVersionProcessingResult{diagnostics: diagnostics}
|
|
}
|
|
result := sourceVersionProcessingResult{payload: payload, imports: imports}
|
|
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,
|
|
absence: resolved.absence,
|
|
}
|
|
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 resolved.present && sourceVersionControlOverlap(root, evidence.resolvedPath) {
|
|
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
|
|
}
|
|
}
|
|
|
|
evidence.checked = true
|
|
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 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{kind: current.Kind})
|
|
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)}, kind: current.Kind,
|
|
})
|
|
}
|
|
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
|
|
}
|