Clarify filesystem directive and override ownership
This commit is contained in:
11
compose.go
11
compose.go
@@ -15,6 +15,17 @@ type composedDocument struct {
|
|||||||
inputNodes int
|
inputNodes int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cloneComposedDocument detaches mutable composition and provenance state;
|
||||||
|
// history application trees are immutable and retain their existing sharing.
|
||||||
|
func cloneComposedDocument(base *composedDocument) *composedDocument {
|
||||||
|
return &composedDocument{
|
||||||
|
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
||||||
|
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
||||||
|
applications: cloneHistoryApplications(base.applications),
|
||||||
|
inputBytes: base.inputBytes, inputNodes: base.inputNodes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type compositionClaim struct {
|
type compositionClaim struct {
|
||||||
path Path
|
path Path
|
||||||
kind document.Kind
|
kind document.Kind
|
||||||
|
|||||||
227
directives.go
Normal file
227
directives.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
type importDeclaration struct {
|
||||||
|
path string
|
||||||
|
glob string
|
||||||
|
optional bool
|
||||||
|
location Location
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitImportDirectives(root *document.Node) (*document.Node, []importDeclaration, []Diagnostic) {
|
||||||
|
body := document.Clone(root)
|
||||||
|
var declarations []importDeclaration
|
||||||
|
directiveIndex := -1
|
||||||
|
for index, entry := range body.Mapping {
|
||||||
|
if entry.Key != "_configkit" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
directiveIndex = index
|
||||||
|
parsed, diagnostics := parseImportDirective(entry.Value, Keys("_configkit"))
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return nil, nil, diagnostics
|
||||||
|
}
|
||||||
|
declarations = parsed
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if directiveIndex >= 0 {
|
||||||
|
body.Mapping = append(body.Mapping[:directiveIndex], body.Mapping[directiveIndex+1:]...)
|
||||||
|
}
|
||||||
|
if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 {
|
||||||
|
return nil, nil, diagnostics
|
||||||
|
}
|
||||||
|
return body, declarations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseImportDirective(node *document.Node, directivePath Path) ([]importDeclaration, []Diagnostic) {
|
||||||
|
if node.Kind != document.Mapping {
|
||||||
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)}
|
||||||
|
}
|
||||||
|
var declarations []importDeclaration
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
fieldPath := directivePath.Key(entry.Key)
|
||||||
|
if entry.Key != "imports" {
|
||||||
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value)}
|
||||||
|
}
|
||||||
|
if entry.Value.Kind != document.Sequence {
|
||||||
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value)}
|
||||||
|
}
|
||||||
|
for index, item := range entry.Value.Sequence {
|
||||||
|
declaration, diagnostics := parseImportDeclaration(item, fieldPath.Index(index))
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return nil, diagnostics
|
||||||
|
}
|
||||||
|
declarations = append(declarations, declaration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return declarations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseImportDeclaration(node *document.Node, declarationPath Path) (importDeclaration, []Diagnostic) {
|
||||||
|
if node.Kind != document.Mapping {
|
||||||
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)}
|
||||||
|
}
|
||||||
|
declaration := importDeclaration{location: publicLocation(node.Location)}
|
||||||
|
seenTarget := false
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
fieldPath := declarationPath.Key(entry.Key)
|
||||||
|
switch entry.Key {
|
||||||
|
case "path", "glob":
|
||||||
|
if seenTarget || entry.Value.Kind != document.String || entry.Value.Text == "" || strings.TrimSpace(entry.Value.Text) != entry.Value.Text {
|
||||||
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration requires exactly one nonempty path or glob string", entry.Value)}
|
||||||
|
}
|
||||||
|
seenTarget = true
|
||||||
|
declaration.location = publicLocation(entry.Value.Location)
|
||||||
|
if entry.Key == "path" {
|
||||||
|
declaration.path = entry.Value.Text
|
||||||
|
} else {
|
||||||
|
declaration.glob = entry.Value.Text
|
||||||
|
}
|
||||||
|
case "optional":
|
||||||
|
if entry.Value.Kind != document.Boolean {
|
||||||
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "optional must be a boolean", entry.Value)}
|
||||||
|
}
|
||||||
|
declaration.optional = entry.Value.Text == "true"
|
||||||
|
default:
|
||||||
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seenTarget {
|
||||||
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path or glob", node)}
|
||||||
|
}
|
||||||
|
return declaration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
|
||||||
|
if node.Kind == document.Mapping {
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
childPath := current.Key(entry.Key)
|
||||||
|
if entry.Key == "_configkit" {
|
||||||
|
location := publicLocation(entry.KeyLocation)
|
||||||
|
return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}}
|
||||||
|
}
|
||||||
|
if diagnostics := nestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if node.Kind == document.Sequence {
|
||||||
|
for index, child := range node.Sequence {
|
||||||
|
if diagnostics := nestedDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
|
||||||
|
if rule == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
current := root
|
||||||
|
if len(rule.Path.segments) == 0 {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
for _, segment := range rule.Path.segments {
|
||||||
|
if current.Kind != document.Mapping {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
if segment.kind == pathSegmentIndex {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key := segment.key
|
||||||
|
index := documentEntryIndex(current.Mapping, key)
|
||||||
|
if index < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
current = current.Mapping[index].Value
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
func validImportPath(value string) bool {
|
||||||
|
if value == "" || strings.TrimSpace(value) != value || path.IsAbs(value) || strings.HasPrefix(value, "//") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !hasImportScheme(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasImportScheme(value string) bool {
|
||||||
|
colon := strings.IndexByte(value, ':')
|
||||||
|
slash := strings.IndexByte(value, '/')
|
||||||
|
if colon <= 0 || (slash >= 0 && slash < colon) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := 0; index < colon; index++ {
|
||||||
|
character := value[index]
|
||||||
|
if index == 0 {
|
||||||
|
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') &&
|
||||||
|
(character < '0' || character > '9') && character != '+' && character != '-' && character != '.' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeImportDiagnostic(code Code, path Path, message string, node *document.Node) Diagnostic {
|
||||||
|
location := publicLocation(node.Location)
|
||||||
|
return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
|
||||||
|
if node.Kind == document.Mapping {
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
childPath := current.Key(entry.Key)
|
||||||
|
if entry.Key == "_configkit" {
|
||||||
|
location := publicLocation(entry.KeyLocation)
|
||||||
|
return []Diagnostic{{
|
||||||
|
Code: CodeProfileDirective, Severity: SeverityError, Path: childPath,
|
||||||
|
Message: "profiles cannot contain configuration directives", Primary: &location,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
if diagnostics := profileDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if node.Kind == document.Sequence {
|
||||||
|
for index, child := range node.Sequence {
|
||||||
|
if diagnostics := profileDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
if node.Kind == document.Mapping {
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
childPath := path.Key(entry.Key)
|
||||||
|
if entry.Key == "_configkit" {
|
||||||
|
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
|
||||||
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
|
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
|
||||||
|
Message: "configuration directives require imports to be enabled", Primary: &location,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(entry.Value, childPath)...)
|
||||||
|
}
|
||||||
|
} else if node.Kind == document.Sequence {
|
||||||
|
for index, child := range node.Sequence {
|
||||||
|
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(child, path.Index(index))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
@@ -19,8 +19,9 @@ module and package documentation boundary. Its implemented public primitives are
|
|||||||
- fixed selected-profile loading in `profile.go` and recursive, non-aliasing overlay
|
- fixed selected-profile loading in `profile.go` and recursive, non-aliasing overlay
|
||||||
with displacement records in `overlay.go`; overlay records retain origins,
|
with displacement records in `overlay.go`; overlay records retain origins,
|
||||||
while value explanation uses separate history applications;
|
while value explanation uses separate history applications;
|
||||||
- detached parser-neutral CLI override validation and synthetic-source application
|
- CLI override validation/preparation in `override.go`, parser-neutral values in
|
||||||
in `override.go`, sharing overlay, source, byte, node, and depth policies;
|
`override_value.go`, and synthetic-source application in `override_apply.go`,
|
||||||
|
sharing overlay, source, byte, node, and depth policies;
|
||||||
- a per-operation private reflection descriptor graph in `schema.go` for supported
|
- a per-operation private reflection descriptor graph in `schema.go` for supported
|
||||||
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
|
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
|
||||||
- descriptor-driven canonical projection in `projection.go`, detached typed
|
- descriptor-driven canonical projection in `projection.go`, detached typed
|
||||||
@@ -31,7 +32,9 @@ module and package documentation boundary. Its implemented public primitives are
|
|||||||
mapping/sequence semantics, null and numeric policy, safe codec errors, presence,
|
mapping/sequence semantics, null and numeric policy, safe codec errors, presence,
|
||||||
baseline/final trees, and initial default/input origins;
|
baseline/final trees, and initial default/input origins;
|
||||||
- public lifecycle coordination in `load.go`, bounded canonical import DFS in
|
- public lifecycle coordination in `load.go`, bounded canonical import DFS in
|
||||||
`imports.go`, pattern-directed glob expansion in `glob.go`, version-report contexts
|
`imports.go`, filesystem context/containment in `filesystem.go`, directive
|
||||||
|
parsing/restrictions in `directives.go`, pattern-directed glob expansion in
|
||||||
|
`glob.go`, version-report contexts
|
||||||
in `validation.go`, and detached layered metadata/history assembly in
|
in `validation.go`, and detached layered metadata/history assembly in
|
||||||
`load_metadata.go`;
|
`load_metadata.go`;
|
||||||
- recorded application defaulting and normalization in `resolution.go`, including
|
- recorded application defaulting and normalization in `resolution.go`, including
|
||||||
|
|||||||
@@ -249,7 +249,16 @@ borrowed values or reread the environment. Run race tests for this unit.
|
|||||||
|
|
||||||
## Stage 6 — Clarify source assembly and override ownership
|
## Stage 6 — Clarify source assembly and override ownership
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
|
Completion: filesystem context/canonicalization/containment moved to
|
||||||
|
`filesystem.go`; directive parsing and restrictions moved to `directives.go`;
|
||||||
|
input node counting now shares the document owner with explicit key accounting.
|
||||||
|
Override preparation, parser-neutral value building, and application are separate;
|
||||||
|
profile and CLI application share the composition clone. Deferred profile cleanup
|
||||||
|
preserves primary failures and explicitly checks the success-path close once.
|
||||||
|
Full offline tests, vet, build, and whitespace checks passed with Go 1.26.7 on
|
||||||
|
Linux/amd64, including imports/globs, profiles, CLI, limits, and provenance.
|
||||||
|
|
||||||
Evidence: [imports.go](../../imports.go) mixes filesystem context, directives,
|
Evidence: [imports.go](../../imports.go) mixes filesystem context, directives,
|
||||||
graph traversal, and diagnostics; [override.go](../../override.go) combines value
|
graph traversal, and diagnostics; [override.go](../../override.go) combines value
|
||||||
|
|||||||
@@ -64,3 +64,23 @@ func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
|
|||||||
}
|
}
|
||||||
return current, true
|
return current, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// countInputNodes includes mapping-key nodes, unlike walkDocument's value-only
|
||||||
|
// traversal. Aggregate source budgets charge every input occurrence.
|
||||||
|
func countInputNodes(node *document.Node) int {
|
||||||
|
if node == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
count := 1
|
||||||
|
if node.Kind == document.Mapping {
|
||||||
|
count += len(node.Mapping)
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
count += countInputNodes(entry.Value)
|
||||||
|
}
|
||||||
|
} else if node.Kind == document.Sequence {
|
||||||
|
for _, child := range node.Sequence {
|
||||||
|
count += countInputNodes(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|||||||
121
filesystem.go
Normal file
121
filesystem.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateFilesystemContext(source Source, context *FilesystemContext, main parsedSource) *Diagnostic {
|
||||||
|
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
|
||||||
|
if diagnostic != nil {
|
||||||
|
return diagnostic
|
||||||
|
}
|
||||||
|
root, err := os.OpenRoot(rootPath)
|
||||||
|
if err != nil {
|
||||||
|
value := globFilesystemDiagnostic(err, "configuration root could not be opened", Location{})
|
||||||
|
value.Primary = nil
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
if err := root.Close(); err != nil {
|
||||||
|
value := importDiagnostic(CodeImportRead, "configuration root could not be closed", nil)
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filesystemOptionDiagnostics(source Source, context *FilesystemContext) []Diagnostic {
|
||||||
|
switch source.kind {
|
||||||
|
case sourceNone:
|
||||||
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "NoSource cannot use filesystem context")}
|
||||||
|
case sourceFile:
|
||||||
|
if context.BaseDir != "" {
|
||||||
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")}
|
||||||
|
}
|
||||||
|
case sourceBytes, sourceReader:
|
||||||
|
if context.BaseDir == "" || context.Root == "" {
|
||||||
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem context requires base and root directories")}
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) {
|
||||||
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem base and root directories must be absolute")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func importFilesystem(source Source, context *FilesystemContext, main parsedSource) (rootPath, basePath, mainPath string, diagnostic *Diagnostic) {
|
||||||
|
if main.source == nil || main.document == nil {
|
||||||
|
value := importDiagnostic(CodeImportInvalid, "imports require a selected configuration source", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.kind == sourceFile {
|
||||||
|
if context != nil && context.BaseDir != "" {
|
||||||
|
value := optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
canonical, err := canonicalExistingPath(source.path)
|
||||||
|
if err != nil {
|
||||||
|
value := importDiagnostic(CodeImportInvalid, "selected source path could not be canonicalized", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
mainPath = canonical
|
||||||
|
basePath = filepath.Dir(canonical)
|
||||||
|
rootPath = basePath
|
||||||
|
if context != nil && context.Root != "" {
|
||||||
|
rootPath, err = canonicalExistingPath(context.Root)
|
||||||
|
if err != nil {
|
||||||
|
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if context == nil || context.BaseDir == "" || context.Root == "" {
|
||||||
|
value := optionDiagnostic(CodeOptionsInvalid, "byte and reader imports require absolute base and root directories")
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) {
|
||||||
|
value := optionDiagnostic(CodeOptionsInvalid, "byte and reader import base and root directories must be absolute")
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
basePath, err = canonicalExistingPath(context.BaseDir)
|
||||||
|
if err != nil {
|
||||||
|
value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
rootPath, err = canonicalExistingPath(context.Root)
|
||||||
|
if err != nil {
|
||||||
|
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !containedPath(rootPath, basePath) || (mainPath != "" && !containedPath(rootPath, mainPath)) {
|
||||||
|
value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
for _, candidate := range []string{rootPath, basePath} {
|
||||||
|
info, err := os.Stat(candidate)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
value := importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil)
|
||||||
|
return "", "", "", &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rootPath, basePath, mainPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalExistingPath(name string) (string, error) {
|
||||||
|
absolute, err := filepath.Abs(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.EvalSymlinks(filepath.Clean(absolute))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containedPath(root, target string) bool {
|
||||||
|
relative, err := filepath.Rel(root, target)
|
||||||
|
if err != nil || filepath.IsAbs(relative) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
|
||||||
|
}
|
||||||
304
imports.go
304
imports.go
@@ -35,13 +35,6 @@ func defaultImportLimits() importLimits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type importDeclaration struct {
|
|
||||||
path string
|
|
||||||
glob string
|
|
||||||
optional bool
|
|
||||||
location Location
|
|
||||||
}
|
|
||||||
|
|
||||||
type importRoute struct {
|
type importRoute struct {
|
||||||
canonical string
|
canonical string
|
||||||
locations []Location
|
locations []Location
|
||||||
@@ -120,120 +113,6 @@ func loadImportedDocuments(source Source, context *FilesystemContext, main parse
|
|||||||
return composed, cloneSourceInfos(state.sources), diagnostics
|
return composed, cloneSourceInfos(state.sources), diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFilesystemContext(source Source, context *FilesystemContext, main parsedSource) *Diagnostic {
|
|
||||||
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
|
|
||||||
if diagnostic != nil {
|
|
||||||
return diagnostic
|
|
||||||
}
|
|
||||||
root, err := os.OpenRoot(rootPath)
|
|
||||||
if err != nil {
|
|
||||||
value := globFilesystemDiagnostic(err, "configuration root could not be opened", Location{})
|
|
||||||
value.Primary = nil
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
if err := root.Close(); err != nil {
|
|
||||||
value := importDiagnostic(CodeImportRead, "configuration root could not be closed", nil)
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filesystemOptionDiagnostics(source Source, context *FilesystemContext) []Diagnostic {
|
|
||||||
switch source.kind {
|
|
||||||
case sourceNone:
|
|
||||||
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "NoSource cannot use filesystem context")}
|
|
||||||
case sourceFile:
|
|
||||||
if context.BaseDir != "" {
|
|
||||||
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")}
|
|
||||||
}
|
|
||||||
case sourceBytes, sourceReader:
|
|
||||||
if context.BaseDir == "" || context.Root == "" {
|
|
||||||
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem context requires base and root directories")}
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) {
|
|
||||||
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem base and root directories must be absolute")}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func importFilesystem(source Source, context *FilesystemContext, main parsedSource) (rootPath, basePath, mainPath string, diagnostic *Diagnostic) {
|
|
||||||
if main.source == nil || main.document == nil {
|
|
||||||
value := importDiagnostic(CodeImportInvalid, "imports require a selected configuration source", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
|
|
||||||
if source.kind == sourceFile {
|
|
||||||
if context != nil && context.BaseDir != "" {
|
|
||||||
value := optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
canonical, err := canonicalExistingPath(source.path)
|
|
||||||
if err != nil {
|
|
||||||
value := importDiagnostic(CodeImportInvalid, "selected source path could not be canonicalized", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
mainPath = canonical
|
|
||||||
basePath = filepath.Dir(canonical)
|
|
||||||
rootPath = basePath
|
|
||||||
if context != nil && context.Root != "" {
|
|
||||||
rootPath, err = canonicalExistingPath(context.Root)
|
|
||||||
if err != nil {
|
|
||||||
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if context == nil || context.BaseDir == "" || context.Root == "" {
|
|
||||||
value := optionDiagnostic(CodeOptionsInvalid, "byte and reader imports require absolute base and root directories")
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) {
|
|
||||||
value := optionDiagnostic(CodeOptionsInvalid, "byte and reader import base and root directories must be absolute")
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
basePath, err = canonicalExistingPath(context.BaseDir)
|
|
||||||
if err != nil {
|
|
||||||
value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
rootPath, err = canonicalExistingPath(context.Root)
|
|
||||||
if err != nil {
|
|
||||||
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !containedPath(rootPath, basePath) || (mainPath != "" && !containedPath(rootPath, mainPath)) {
|
|
||||||
value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
for _, candidate := range []string{rootPath, basePath} {
|
|
||||||
info, err := os.Stat(candidate)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
value := importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil)
|
|
||||||
return "", "", "", &value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rootPath, basePath, mainPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalExistingPath(name string) (string, error) {
|
|
||||||
absolute, err := filepath.Abs(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.EvalSymlinks(filepath.Clean(absolute))
|
|
||||||
}
|
|
||||||
|
|
||||||
func containedPath(root, target string) bool {
|
|
||||||
relative, err := filepath.Rel(root, target)
|
|
||||||
if err != nil || filepath.IsAbs(relative) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *importTraversal) checkInitialLimits() *Diagnostic {
|
func (s *importTraversal) checkInitialLimits() *Diagnostic {
|
||||||
if s.limits.sources < 1 {
|
if s.limits.sources < 1 {
|
||||||
value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil)
|
value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil)
|
||||||
@@ -404,184 +283,6 @@ func (s *importTraversal) currentRoute(location Location) importRoute {
|
|||||||
return importRoute{locations: locations}
|
return importRoute{locations: locations}
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitImportDirectives(root *document.Node) (*document.Node, []importDeclaration, []Diagnostic) {
|
|
||||||
body := document.Clone(root)
|
|
||||||
var declarations []importDeclaration
|
|
||||||
directiveIndex := -1
|
|
||||||
for index, entry := range body.Mapping {
|
|
||||||
if entry.Key != "_configkit" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
directiveIndex = index
|
|
||||||
parsed, diagnostics := parseImportDirective(entry.Value, Keys("_configkit"))
|
|
||||||
if len(diagnostics) != 0 {
|
|
||||||
return nil, nil, diagnostics
|
|
||||||
}
|
|
||||||
declarations = parsed
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if directiveIndex >= 0 {
|
|
||||||
body.Mapping = append(body.Mapping[:directiveIndex], body.Mapping[directiveIndex+1:]...)
|
|
||||||
}
|
|
||||||
if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 {
|
|
||||||
return nil, nil, diagnostics
|
|
||||||
}
|
|
||||||
return body, declarations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseImportDirective(node *document.Node, directivePath Path) ([]importDeclaration, []Diagnostic) {
|
|
||||||
if node.Kind != document.Mapping {
|
|
||||||
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)}
|
|
||||||
}
|
|
||||||
var declarations []importDeclaration
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
fieldPath := directivePath.Key(entry.Key)
|
|
||||||
if entry.Key != "imports" {
|
|
||||||
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value)}
|
|
||||||
}
|
|
||||||
if entry.Value.Kind != document.Sequence {
|
|
||||||
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value)}
|
|
||||||
}
|
|
||||||
for index, item := range entry.Value.Sequence {
|
|
||||||
declaration, diagnostics := parseImportDeclaration(item, fieldPath.Index(index))
|
|
||||||
if len(diagnostics) != 0 {
|
|
||||||
return nil, diagnostics
|
|
||||||
}
|
|
||||||
declarations = append(declarations, declaration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return declarations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseImportDeclaration(node *document.Node, declarationPath Path) (importDeclaration, []Diagnostic) {
|
|
||||||
if node.Kind != document.Mapping {
|
|
||||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)}
|
|
||||||
}
|
|
||||||
declaration := importDeclaration{location: publicLocation(node.Location)}
|
|
||||||
seenTarget := false
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
fieldPath := declarationPath.Key(entry.Key)
|
|
||||||
switch entry.Key {
|
|
||||||
case "path", "glob":
|
|
||||||
if seenTarget || entry.Value.Kind != document.String || entry.Value.Text == "" || strings.TrimSpace(entry.Value.Text) != entry.Value.Text {
|
|
||||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration requires exactly one nonempty path or glob string", entry.Value)}
|
|
||||||
}
|
|
||||||
seenTarget = true
|
|
||||||
declaration.location = publicLocation(entry.Value.Location)
|
|
||||||
if entry.Key == "path" {
|
|
||||||
declaration.path = entry.Value.Text
|
|
||||||
} else {
|
|
||||||
declaration.glob = entry.Value.Text
|
|
||||||
}
|
|
||||||
case "optional":
|
|
||||||
if entry.Value.Kind != document.Boolean {
|
|
||||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "optional must be a boolean", entry.Value)}
|
|
||||||
}
|
|
||||||
declaration.optional = entry.Value.Text == "true"
|
|
||||||
default:
|
|
||||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !seenTarget {
|
|
||||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path or glob", node)}
|
|
||||||
}
|
|
||||||
return declaration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
|
|
||||||
if node.Kind == document.Mapping {
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
childPath := current.Key(entry.Key)
|
|
||||||
if entry.Key == "_configkit" {
|
|
||||||
location := publicLocation(entry.KeyLocation)
|
|
||||||
return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}}
|
|
||||||
}
|
|
||||||
if diagnostics := nestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node.Kind == document.Sequence {
|
|
||||||
for index, child := range node.Sequence {
|
|
||||||
if diagnostics := nestedDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
|
|
||||||
if rule == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
current := root
|
|
||||||
if len(rule.Path.segments) == 0 {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
for _, segment := range rule.Path.segments {
|
|
||||||
if current.Kind != document.Mapping {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
if segment.kind == pathSegmentIndex {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
key := segment.key
|
|
||||||
index := documentEntryIndex(current.Mapping, key)
|
|
||||||
if index < 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
current = current.Mapping[index].Value
|
|
||||||
}
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
func countInputNodes(node *document.Node) int {
|
|
||||||
if node == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
count := 1
|
|
||||||
if node.Kind == document.Mapping {
|
|
||||||
count += len(node.Mapping)
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
count += countInputNodes(entry.Value)
|
|
||||||
}
|
|
||||||
} else if node.Kind == document.Sequence {
|
|
||||||
for _, child := range node.Sequence {
|
|
||||||
count += countInputNodes(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func validImportPath(value string) bool {
|
|
||||||
if value == "" || strings.TrimSpace(value) != value || path.IsAbs(value) || strings.HasPrefix(value, "//") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !hasImportScheme(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasImportScheme(value string) bool {
|
|
||||||
colon := strings.IndexByte(value, ':')
|
|
||||||
slash := strings.IndexByte(value, '/')
|
|
||||||
if colon <= 0 || (slash >= 0 && slash < colon) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for index := 0; index < colon; index++ {
|
|
||||||
character := value[index]
|
|
||||||
if index == 0 {
|
|
||||||
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') &&
|
|
||||||
(character < '0' || character > '9') && character != '+' && character != '-' && character != '.' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func sourceInfos(source *SourceInfo) []SourceInfo {
|
func sourceInfos(source *SourceInfo) []SourceInfo {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -593,11 +294,6 @@ func importDiagnostic(code Code, message string, location *Location) Diagnostic
|
|||||||
return Diagnostic{Code: code, Severity: SeverityError, Message: message, Primary: cloneLocation(location)}
|
return Diagnostic{Code: code, Severity: SeverityError, Message: message, Primary: cloneLocation(location)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeImportDiagnostic(code Code, path Path, message string, node *document.Node) Diagnostic {
|
|
||||||
location := publicLocation(node.Location)
|
|
||||||
return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location}
|
|
||||||
}
|
|
||||||
|
|
||||||
func importCycleDiagnostic(target string, location Location, cycle []importRoute) Diagnostic {
|
func importCycleDiagnostic(target string, location Location, cycle []importRoute) Diagnostic {
|
||||||
paths := make([]string, 0, len(cycle)+1)
|
paths := make([]string, 0, len(cycle)+1)
|
||||||
related := make([]Location, 0, len(cycle))
|
related := make([]Location, 0, len(cycle))
|
||||||
|
|||||||
22
load.go
22
load.go
@@ -339,28 +339,6 @@ func optionDiagnostic(code Code, message string) Diagnostic {
|
|||||||
return Diagnostic{Code: code, Severity: SeverityError, Message: message}
|
return Diagnostic{Code: code, Severity: SeverityError, Message: message}
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
|
|
||||||
var diagnostics []Diagnostic
|
|
||||||
if node.Kind == document.Mapping {
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
childPath := path.Key(entry.Key)
|
|
||||||
if entry.Key == "_configkit" {
|
|
||||||
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
|
|
||||||
diagnostics = append(diagnostics, Diagnostic{
|
|
||||||
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
|
|
||||||
Message: "configuration directives require imports to be enabled", Primary: &location,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(entry.Value, childPath)...)
|
|
||||||
}
|
|
||||||
} else if node.Kind == document.Sequence {
|
|
||||||
for index, child := range node.Sequence {
|
|
||||||
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(child, path.Index(index))...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic {
|
func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic {
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
10
overlay.go
10
overlay.go
@@ -24,15 +24,7 @@ func overlayDocuments(base *composedDocument, overlay *document.Node) (*composed
|
|||||||
Message: "profile overlay requires mapping document roots",
|
Message: "profile overlay requires mapping document roots",
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
result := &composedDocument{
|
result := cloneComposedDocument(base)
|
||||||
root: document.Clone(base.root),
|
|
||||||
claims: cloneCompositionClaims(base.claims),
|
|
||||||
origins: cloneOriginIndex(base.origins),
|
|
||||||
overlays: cloneOverlayRecords(base.overlays),
|
|
||||||
applications: cloneHistoryApplications(base.applications),
|
|
||||||
inputBytes: base.inputBytes,
|
|
||||||
inputNodes: base.inputNodes,
|
|
||||||
}
|
|
||||||
mergeOverlayOrigin(result.origins, Path{}, overlay.Location, LayerProfile)
|
mergeOverlayOrigin(result.origins, Path{}, overlay.Location, LayerProfile)
|
||||||
mergeOverlayMappings(result, result.root, overlay, Path{}, LayerProfile, true)
|
mergeOverlayMappings(result, result.root, overlay, Path{}, LayerProfile, true)
|
||||||
result.applications = append(result.applications, historyApplication{layer: LayerProfile, value: overlay})
|
result.applications = append(result.applications, historyApplication{layer: LayerProfile, value: overlay})
|
||||||
|
|||||||
240
override.go
240
override.go
@@ -1,9 +1,6 @@
|
|||||||
package configkit
|
package configkit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -22,17 +19,7 @@ type preparedOverride struct {
|
|||||||
nodes int
|
nodes int
|
||||||
}
|
}
|
||||||
|
|
||||||
type overrideValueBuilder struct {
|
func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) ([]preparedOverride, []SourceInfo, []Diagnostic) {
|
||||||
sourceID string
|
|
||||||
active map[valueVisit]bool
|
|
||||||
bytes int64
|
|
||||||
nodes int
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
||||||
if len(overrides) == 0 {
|
|
||||||
return base, cloneSourceInfos(sources), nil
|
|
||||||
}
|
|
||||||
if len(overrides) > maxOverrides || len(sources)+len(overrides) > limits.sources {
|
if len(overrides) > maxOverrides || len(sources)+len(overrides) > limits.sources {
|
||||||
return nil, cloneSourceInfos(sources), []Diagnostic{{
|
return nil, cloneSourceInfos(sources), []Diagnostic{{
|
||||||
Code: CodeOverrideLimitExceeded, Severity: SeverityError,
|
Code: CodeOverrideLimitExceeded, Severity: SeverityError,
|
||||||
@@ -147,13 +134,7 @@ func applyOverrides(base *composedDocument, sources []SourceInfo, overrides []Ov
|
|||||||
return nil, allSources, canonicalDiagnostics(diagnostics)
|
return nil, allSources, canonicalDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := cloneComposedDocument(base)
|
return prepared, allSources, nil
|
||||||
for _, override := range prepared {
|
|
||||||
applyPreparedOverride(result, override)
|
|
||||||
}
|
|
||||||
result.inputBytes += usedBytes
|
|
||||||
result.inputNodes += usedNodes
|
|
||||||
return result, allSources, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validOverrideLabel(label string) bool {
|
func validOverrideLabel(label string) bool {
|
||||||
@@ -240,220 +221,3 @@ func overrideDirectivePath(node *document.Node, path Path) (Path, bool) {
|
|||||||
}
|
}
|
||||||
return Path{}, false
|
return Path{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *overrideValueBuilder) build(value any, path Path, depth int) (*document.Node, *Diagnostic) {
|
|
||||||
if depth > maxDocumentDepth {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the depth limit")
|
|
||||||
}
|
|
||||||
b.nodes++
|
|
||||||
if b.nodes > maxDocumentNodes {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
|
||||||
}
|
|
||||||
location := document.Location{SourceID: b.sourceID}
|
|
||||||
switch value := value.(type) {
|
|
||||||
case nil:
|
|
||||||
return &document.Node{Kind: document.Null, Location: location}, nil
|
|
||||||
case string:
|
|
||||||
if !b.addBytes(len(value)) {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the record string-byte limit")
|
|
||||||
}
|
|
||||||
return &document.Node{Kind: document.String, Text: value, Location: location}, nil
|
|
||||||
case bool:
|
|
||||||
return &document.Node{Kind: document.Boolean, Text: strconv.FormatBool(value), Location: location}, nil
|
|
||||||
case int:
|
|
||||||
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
||||||
case int8:
|
|
||||||
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
||||||
case int16:
|
|
||||||
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
||||||
case int32:
|
|
||||||
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
||||||
case int64:
|
|
||||||
return overrideNumber(strconv.FormatInt(value, 10), location), nil
|
|
||||||
case uint:
|
|
||||||
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
||||||
case uint8:
|
|
||||||
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
||||||
case uint16:
|
|
||||||
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
||||||
case uint32:
|
|
||||||
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
||||||
case uint64:
|
|
||||||
return overrideNumber(strconv.FormatUint(value, 10), location), nil
|
|
||||||
case float32:
|
|
||||||
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite")
|
|
||||||
}
|
|
||||||
return overrideNumber(strconv.FormatFloat(float64(value), 'g', -1, 32), location), nil
|
|
||||||
case float64:
|
|
||||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite")
|
|
||||||
}
|
|
||||||
return overrideNumber(strconv.FormatFloat(value, 'g', -1, 64), location), nil
|
|
||||||
case []any:
|
|
||||||
if value == nil {
|
|
||||||
return &document.Node{Kind: document.Null, Location: location}, nil
|
|
||||||
}
|
|
||||||
leave, diagnostic := b.enter(reflect.ValueOf(value), path)
|
|
||||||
if diagnostic != nil {
|
|
||||||
return nil, diagnostic
|
|
||||||
}
|
|
||||||
defer leave()
|
|
||||||
if len(value) > maxDocumentNodes-b.nodes {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
|
||||||
}
|
|
||||||
node := &document.Node{Kind: document.Sequence, Location: location, Sequence: make([]*document.Node, len(value))}
|
|
||||||
for index, child := range value {
|
|
||||||
built, diagnostic := b.build(child, path.Index(index), depth+1)
|
|
||||||
if diagnostic != nil {
|
|
||||||
return nil, diagnostic
|
|
||||||
}
|
|
||||||
node.Sequence[index] = built
|
|
||||||
}
|
|
||||||
return node, nil
|
|
||||||
case map[string]any:
|
|
||||||
if value == nil {
|
|
||||||
return &document.Node{Kind: document.Null, Location: location}, nil
|
|
||||||
}
|
|
||||||
leave, diagnostic := b.enter(reflect.ValueOf(value), path)
|
|
||||||
if diagnostic != nil {
|
|
||||||
return nil, diagnostic
|
|
||||||
}
|
|
||||||
defer leave()
|
|
||||||
if len(value) > (maxDocumentNodes-b.nodes)/2 {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
|
||||||
}
|
|
||||||
keys := make([]string, 0, len(value))
|
|
||||||
for key := range value {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
slices.Sort(keys)
|
|
||||||
node := &document.Node{Kind: document.Mapping, Location: location, Mapping: make([]document.Entry, 0, len(keys))}
|
|
||||||
for _, key := range keys {
|
|
||||||
b.nodes++ // mapping key
|
|
||||||
if !b.addBytes(len(key)) {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path.Key(key), "configuration override exceeds the record string-byte limit")
|
|
||||||
}
|
|
||||||
built, diagnostic := b.build(value[key], path.Key(key), depth+1)
|
|
||||||
if diagnostic != nil {
|
|
||||||
return nil, diagnostic
|
|
||||||
}
|
|
||||||
node.Mapping = append(node.Mapping, document.Entry{Key: key, KeyLocation: location, ValueLocation: location, Value: built})
|
|
||||||
}
|
|
||||||
return node, nil
|
|
||||||
default:
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override value has an unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *overrideValueBuilder) addBytes(count int) bool {
|
|
||||||
if count < 0 || int64(count) > maxSourceBytes-b.bytes {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
b.bytes += int64(count)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *overrideValueBuilder) enter(value reflect.Value, path Path) (func(), *Diagnostic) {
|
|
||||||
visit, ok := visitFor(value)
|
|
||||||
if !ok {
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
if b.active[visit] {
|
|
||||||
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override values must not contain cycles")
|
|
||||||
}
|
|
||||||
b.active[visit] = true
|
|
||||||
return func() { delete(b.active, visit) }, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func overrideValueDiagnostic(code Code, path Path, message string) *Diagnostic {
|
|
||||||
return &Diagnostic{Code: code, Severity: SeverityError, Path: path.clone(), Message: message}
|
|
||||||
}
|
|
||||||
|
|
||||||
func overrideNumber(text string, location document.Location) *document.Node {
|
|
||||||
return &document.Node{Kind: document.Number, Text: text, Location: location}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneComposedDocument(base *composedDocument) *composedDocument {
|
|
||||||
return &composedDocument{
|
|
||||||
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
|
||||||
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
|
||||||
applications: cloneHistoryApplications(base.applications),
|
|
||||||
inputBytes: base.inputBytes, inputNodes: base.inputNodes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyPreparedOverride(result *composedDocument, override preparedOverride) {
|
|
||||||
location := document.Location{SourceID: override.source.ID}
|
|
||||||
mergeOverlayOrigin(result.origins, Path{}, location, LayerCLI)
|
|
||||||
current := result.root
|
|
||||||
currentPath := Path{}
|
|
||||||
for index, segment := range override.path.segments {
|
|
||||||
path := currentPath.Key(segment.key)
|
|
||||||
position := documentEntryIndex(current.Mapping, segment.key)
|
|
||||||
if index < len(override.path.segments)-1 {
|
|
||||||
if position < 0 {
|
|
||||||
child := &document.Node{Kind: document.Mapping, Location: location, Mapping: []document.Entry{}}
|
|
||||||
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: child})
|
|
||||||
collectOverlayOrigins(result.origins, child, path, LayerCLI)
|
|
||||||
current = child
|
|
||||||
} else {
|
|
||||||
current = current.Mapping[position].Value
|
|
||||||
mergeOverlayOrigin(result.origins, path, location, LayerCLI)
|
|
||||||
}
|
|
||||||
currentPath = path
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
previous := cloneOrigin(result.origins[path.String()])
|
|
||||||
var displaced []displacedDefinition
|
|
||||||
if position < 0 {
|
|
||||||
incoming := document.Clone(override.value)
|
|
||||||
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: incoming})
|
|
||||||
collectOverlayOrigins(result.origins, incoming, path, LayerCLI)
|
|
||||||
} else {
|
|
||||||
existing := current.Mapping[position].Value
|
|
||||||
displaced = affectedOverrideOrigins(existing, override.value, result.origins, path)
|
|
||||||
if existing.Kind == document.Mapping && override.value.Kind == document.Mapping {
|
|
||||||
mergeOverlayOrigin(result.origins, path, location, LayerCLI)
|
|
||||||
mergeOverlayMappings(result, existing, override.value, path, LayerCLI, false)
|
|
||||||
} else {
|
|
||||||
deleteOriginSubtree(result.origins, path)
|
|
||||||
current.Mapping[position] = document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: document.Clone(override.value)}
|
|
||||||
collectOverlayOrigins(result.origins, current.Mapping[position].Value, path, LayerCLI)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.overlays = append(result.overlays, overlayRecord{change: Change{
|
|
||||||
Path: path.clone(), Origin: overlayOrigin(LayerCLI, location), Previous: previousOrigins(previous),
|
|
||||||
}, displaced: displaced})
|
|
||||||
result.applications = append(result.applications, historyApplication{
|
|
||||||
layer: LayerCLI, path: path.clone(), value: override.value,
|
|
||||||
origin: overlayOrigin(LayerCLI, location),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func affectedOverrideOrigins(existing, incoming *document.Node, origins map[string]Origin, path Path) []displacedDefinition {
|
|
||||||
displaced := []displacedDefinition{{path: path.clone(), origin: cloneOrigin(origins[path.String()])}}
|
|
||||||
if existing.Kind == document.Mapping && incoming.Kind == document.Mapping {
|
|
||||||
for _, entry := range incoming.Mapping {
|
|
||||||
position := documentEntryIndex(existing.Mapping, entry.Key)
|
|
||||||
if position < 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
childPath := path.Key(entry.Key)
|
|
||||||
childExisting := existing.Mapping[position].Value
|
|
||||||
if childExisting.Kind == document.Mapping && entry.Value.Kind == document.Mapping {
|
|
||||||
displaced = append(displaced, affectedOverrideOrigins(childExisting, entry.Value, origins, childPath)...)
|
|
||||||
} else {
|
|
||||||
displaced = append(displaced, displacedOrigins(childExisting, origins, childPath)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
displaced = displacedOrigins(existing, origins, path)
|
|
||||||
}
|
|
||||||
slices.SortFunc(displaced, func(left, right displacedDefinition) int {
|
|
||||||
return strings.Compare(left.path.String(), right.path.String())
|
|
||||||
})
|
|
||||||
return displaced
|
|
||||||
}
|
|
||||||
|
|||||||
100
override_apply.go
Normal file
100
override_apply.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||||
|
if len(overrides) == 0 {
|
||||||
|
return base, cloneSourceInfos(sources), nil
|
||||||
|
}
|
||||||
|
prepared, allSources, diagnostics := prepareOverrides(base, sources, overrides, rule, limits)
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return nil, allSources, diagnostics
|
||||||
|
}
|
||||||
|
result := cloneComposedDocument(base)
|
||||||
|
for _, override := range prepared {
|
||||||
|
applyPreparedOverride(result, override)
|
||||||
|
result.inputBytes += override.bytes
|
||||||
|
result.inputNodes += override.nodes
|
||||||
|
}
|
||||||
|
return result, allSources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyPreparedOverride(result *composedDocument, override preparedOverride) {
|
||||||
|
location := document.Location{SourceID: override.source.ID}
|
||||||
|
mergeOverlayOrigin(result.origins, Path{}, location, LayerCLI)
|
||||||
|
current := result.root
|
||||||
|
currentPath := Path{}
|
||||||
|
for index, segment := range override.path.segments {
|
||||||
|
path := currentPath.Key(segment.key)
|
||||||
|
position := documentEntryIndex(current.Mapping, segment.key)
|
||||||
|
if index < len(override.path.segments)-1 {
|
||||||
|
if position < 0 {
|
||||||
|
child := &document.Node{Kind: document.Mapping, Location: location, Mapping: []document.Entry{}}
|
||||||
|
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: child})
|
||||||
|
collectOverlayOrigins(result.origins, child, path, LayerCLI)
|
||||||
|
current = child
|
||||||
|
} else {
|
||||||
|
current = current.Mapping[position].Value
|
||||||
|
mergeOverlayOrigin(result.origins, path, location, LayerCLI)
|
||||||
|
}
|
||||||
|
currentPath = path
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
previous := cloneOrigin(result.origins[path.String()])
|
||||||
|
var displaced []displacedDefinition
|
||||||
|
if position < 0 {
|
||||||
|
incoming := document.Clone(override.value)
|
||||||
|
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: incoming})
|
||||||
|
collectOverlayOrigins(result.origins, incoming, path, LayerCLI)
|
||||||
|
} else {
|
||||||
|
existing := current.Mapping[position].Value
|
||||||
|
displaced = affectedOverrideOrigins(existing, override.value, result.origins, path)
|
||||||
|
if existing.Kind == document.Mapping && override.value.Kind == document.Mapping {
|
||||||
|
mergeOverlayOrigin(result.origins, path, location, LayerCLI)
|
||||||
|
mergeOverlayMappings(result, existing, override.value, path, LayerCLI, false)
|
||||||
|
} else {
|
||||||
|
deleteOriginSubtree(result.origins, path)
|
||||||
|
current.Mapping[position] = document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: document.Clone(override.value)}
|
||||||
|
collectOverlayOrigins(result.origins, current.Mapping[position].Value, path, LayerCLI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.overlays = append(result.overlays, overlayRecord{change: Change{
|
||||||
|
Path: path.clone(), Origin: overlayOrigin(LayerCLI, location), Previous: previousOrigins(previous),
|
||||||
|
}, displaced: displaced})
|
||||||
|
result.applications = append(result.applications, historyApplication{
|
||||||
|
layer: LayerCLI, path: path.clone(), value: override.value,
|
||||||
|
origin: overlayOrigin(LayerCLI, location),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func affectedOverrideOrigins(existing, incoming *document.Node, origins map[string]Origin, path Path) []displacedDefinition {
|
||||||
|
displaced := []displacedDefinition{{path: path.clone(), origin: cloneOrigin(origins[path.String()])}}
|
||||||
|
if existing.Kind == document.Mapping && incoming.Kind == document.Mapping {
|
||||||
|
for _, entry := range incoming.Mapping {
|
||||||
|
position := documentEntryIndex(existing.Mapping, entry.Key)
|
||||||
|
if position < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childPath := path.Key(entry.Key)
|
||||||
|
childExisting := existing.Mapping[position].Value
|
||||||
|
if childExisting.Kind == document.Mapping && entry.Value.Kind == document.Mapping {
|
||||||
|
displaced = append(displaced, affectedOverrideOrigins(childExisting, entry.Value, origins, childPath)...)
|
||||||
|
} else {
|
||||||
|
displaced = append(displaced, displacedOrigins(childExisting, origins, childPath)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displaced = displacedOrigins(existing, origins, path)
|
||||||
|
}
|
||||||
|
slices.SortFunc(displaced, func(left, right displacedDefinition) int {
|
||||||
|
return strings.Compare(left.path.String(), right.path.String())
|
||||||
|
})
|
||||||
|
return displaced
|
||||||
|
}
|
||||||
150
override_value.go
Normal file
150
override_value.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
type overrideValueBuilder struct {
|
||||||
|
sourceID string
|
||||||
|
active map[valueVisit]bool
|
||||||
|
bytes int64
|
||||||
|
nodes int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *overrideValueBuilder) build(value any, path Path, depth int) (*document.Node, *Diagnostic) {
|
||||||
|
if depth > maxDocumentDepth {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the depth limit")
|
||||||
|
}
|
||||||
|
b.nodes++
|
||||||
|
if b.nodes > maxDocumentNodes {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
||||||
|
}
|
||||||
|
location := document.Location{SourceID: b.sourceID}
|
||||||
|
switch value := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return &document.Node{Kind: document.Null, Location: location}, nil
|
||||||
|
case string:
|
||||||
|
if !b.addBytes(len(value)) {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the record string-byte limit")
|
||||||
|
}
|
||||||
|
return &document.Node{Kind: document.String, Text: value, Location: location}, nil
|
||||||
|
case bool:
|
||||||
|
return &document.Node{Kind: document.Boolean, Text: strconv.FormatBool(value), Location: location}, nil
|
||||||
|
case int:
|
||||||
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
||||||
|
case int8:
|
||||||
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
||||||
|
case int16:
|
||||||
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
||||||
|
case int32:
|
||||||
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
||||||
|
case int64:
|
||||||
|
return overrideNumber(strconv.FormatInt(value, 10), location), nil
|
||||||
|
case uint:
|
||||||
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
||||||
|
case uint8:
|
||||||
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
||||||
|
case uint16:
|
||||||
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
||||||
|
case uint32:
|
||||||
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
||||||
|
case uint64:
|
||||||
|
return overrideNumber(strconv.FormatUint(value, 10), location), nil
|
||||||
|
case float32:
|
||||||
|
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite")
|
||||||
|
}
|
||||||
|
return overrideNumber(strconv.FormatFloat(float64(value), 'g', -1, 32), location), nil
|
||||||
|
case float64:
|
||||||
|
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite")
|
||||||
|
}
|
||||||
|
return overrideNumber(strconv.FormatFloat(value, 'g', -1, 64), location), nil
|
||||||
|
case []any:
|
||||||
|
if value == nil {
|
||||||
|
return &document.Node{Kind: document.Null, Location: location}, nil
|
||||||
|
}
|
||||||
|
leave, diagnostic := b.enter(reflect.ValueOf(value), path)
|
||||||
|
if diagnostic != nil {
|
||||||
|
return nil, diagnostic
|
||||||
|
}
|
||||||
|
defer leave()
|
||||||
|
if len(value) > maxDocumentNodes-b.nodes {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
||||||
|
}
|
||||||
|
node := &document.Node{Kind: document.Sequence, Location: location, Sequence: make([]*document.Node, len(value))}
|
||||||
|
for index, child := range value {
|
||||||
|
built, diagnostic := b.build(child, path.Index(index), depth+1)
|
||||||
|
if diagnostic != nil {
|
||||||
|
return nil, diagnostic
|
||||||
|
}
|
||||||
|
node.Sequence[index] = built
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
case map[string]any:
|
||||||
|
if value == nil {
|
||||||
|
return &document.Node{Kind: document.Null, Location: location}, nil
|
||||||
|
}
|
||||||
|
leave, diagnostic := b.enter(reflect.ValueOf(value), path)
|
||||||
|
if diagnostic != nil {
|
||||||
|
return nil, diagnostic
|
||||||
|
}
|
||||||
|
defer leave()
|
||||||
|
if len(value) > (maxDocumentNodes-b.nodes)/2 {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(value))
|
||||||
|
for key := range value {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
node := &document.Node{Kind: document.Mapping, Location: location, Mapping: make([]document.Entry, 0, len(keys))}
|
||||||
|
for _, key := range keys {
|
||||||
|
b.nodes++ // mapping key
|
||||||
|
if !b.addBytes(len(key)) {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path.Key(key), "configuration override exceeds the record string-byte limit")
|
||||||
|
}
|
||||||
|
built, diagnostic := b.build(value[key], path.Key(key), depth+1)
|
||||||
|
if diagnostic != nil {
|
||||||
|
return nil, diagnostic
|
||||||
|
}
|
||||||
|
node.Mapping = append(node.Mapping, document.Entry{Key: key, KeyLocation: location, ValueLocation: location, Value: built})
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
default:
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override value has an unsupported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *overrideValueBuilder) addBytes(count int) bool {
|
||||||
|
if count < 0 || int64(count) > maxSourceBytes-b.bytes {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b.bytes += int64(count)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *overrideValueBuilder) enter(value reflect.Value, path Path) (func(), *Diagnostic) {
|
||||||
|
visit, ok := visitFor(value)
|
||||||
|
if !ok {
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
if b.active[visit] {
|
||||||
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override values must not contain cycles")
|
||||||
|
}
|
||||||
|
b.active[visit] = true
|
||||||
|
return func() { delete(b.active, visit) }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideValueDiagnostic(code Code, path Path, message string) *Diagnostic {
|
||||||
|
return &Diagnostic{Code: code, Severity: SeverityError, Path: path.clone(), Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideNumber(text string, location document.Location) *document.Node {
|
||||||
|
return &document.Node{Kind: document.Number, Text: text, Location: location}
|
||||||
|
}
|
||||||
42
profile.go
42
profile.go
@@ -9,7 +9,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
||||||
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,9 +66,14 @@ func loadProfileDocument(source Source, context *FilesystemContext, main parsedS
|
|||||||
value.Primary = nil
|
value.Primary = nil
|
||||||
return nil, cloneSourceInfos(sources), []Diagnostic{value}
|
return nil, cloneSourceInfos(sources), []Diagnostic{value}
|
||||||
}
|
}
|
||||||
|
rootClosed := false
|
||||||
|
defer func() {
|
||||||
|
if !rootClosed {
|
||||||
|
_ = root.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
relative, err := filepath.Rel(rootPath, canonical)
|
relative, err := filepath.Rel(rootPath, canonical)
|
||||||
if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
||||||
_ = root.Close()
|
|
||||||
return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileOutsideRoot, "selected profile could not be addressed inside the configuration root", nil)}
|
return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileOutsideRoot, "selected profile could not be addressed inside the configuration root", nil)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,32 +83,29 @@ func loadProfileDocument(source Source, context *FilesystemContext, main parsedS
|
|||||||
sources = append(cloneSourceInfos(sources), profileSource)
|
sources = append(cloneSourceInfos(sources), profileSource)
|
||||||
data, diagnostic := readProfileFile(root, relative, sourceID, limits.bytes-base.inputBytes)
|
data, diagnostic := readProfileFile(root, relative, sourceID, limits.bytes-base.inputBytes)
|
||||||
if diagnostic != nil {
|
if diagnostic != nil {
|
||||||
_ = root.Close()
|
|
||||||
return nil, sources, []Diagnostic{*diagnostic}
|
return nil, sources, []Diagnostic{*diagnostic}
|
||||||
}
|
}
|
||||||
profileRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes})
|
profileRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes})
|
||||||
if len(issues) != 0 {
|
if len(issues) != 0 {
|
||||||
_ = root.Close()
|
|
||||||
return nil, sources, parserDiagnostics(sourceID, issues)
|
return nil, sources, parserDiagnostics(sourceID, issues)
|
||||||
}
|
}
|
||||||
nodeCount := countInputNodes(profileRoot)
|
nodeCount := countInputNodes(profileRoot)
|
||||||
if base.inputNodes+nodeCount > limits.nodes {
|
if base.inputNodes+nodeCount > limits.nodes {
|
||||||
_ = root.Close()
|
|
||||||
return nil, sources, []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the aggregate node limit", profileLocation)}
|
return nil, sources, []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the aggregate node limit", profileLocation)}
|
||||||
}
|
}
|
||||||
if diagnostics := profileDirectiveDiagnostics(profileRoot, Path{}); len(diagnostics) != 0 {
|
if diagnostics := profileDirectiveDiagnostics(profileRoot, Path{}); len(diagnostics) != 0 {
|
||||||
_ = root.Close()
|
|
||||||
return nil, sources, diagnostics
|
return nil, sources, diagnostics
|
||||||
}
|
}
|
||||||
if versionNode := fragmentVersionClaim(profileRoot, rule); versionNode != nil {
|
if versionNode := fragmentVersionClaim(profileRoot, rule); versionNode != nil {
|
||||||
location := publicLocation(versionNode.Location)
|
location := publicLocation(versionNode.Location)
|
||||||
_ = root.Close()
|
|
||||||
return nil, sources, []Diagnostic{{
|
return nil, sources, []Diagnostic{{
|
||||||
Code: CodeProfileVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
Code: CodeProfileVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
||||||
Message: "profiles cannot contribute to or replace the schema-version path", Primary: &location,
|
Message: "profiles cannot contribute to or replace the schema-version path", Primary: &location,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
if err := root.Close(); err != nil {
|
closeErr := root.Close()
|
||||||
|
rootClosed = true
|
||||||
|
if closeErr != nil {
|
||||||
return nil, sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected profile", nil)}
|
return nil, sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected profile", nil)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,28 +129,3 @@ func readProfileFile(root *os.Root, relative, sourceID string, remaining int64)
|
|||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
|
|
||||||
if node.Kind == document.Mapping {
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
childPath := current.Key(entry.Key)
|
|
||||||
if entry.Key == "_configkit" {
|
|
||||||
location := publicLocation(entry.KeyLocation)
|
|
||||||
return []Diagnostic{{
|
|
||||||
Code: CodeProfileDirective, Severity: SeverityError, Path: childPath,
|
|
||||||
Message: "profiles cannot contain configuration directives", Primary: &location,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
if diagnostics := profileDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node.Kind == document.Sequence {
|
|
||||||
for index, child := range node.Sequence {
|
|
||||||
if diagnostics := profileDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user