189 lines
7.0 KiB
Go
189 lines
7.0 KiB
Go
package configkit
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
|
)
|
|
|
|
func activeProfile(name string) bool {
|
|
return name != "" && name != "default"
|
|
}
|
|
|
|
func validProfileName(name string) bool {
|
|
if !activeProfile(name) {
|
|
return true
|
|
}
|
|
for _, character := range name {
|
|
if (character >= 'a' && character <= 'z') ||
|
|
(character >= 'A' && character <= 'Z') ||
|
|
(character >= '0' && character <= '9') || character == '_' || character == '-' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func loadProfileDocument(source Source, context *FilesystemContext, main parsedSource, base *composedDocument, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
|
|
if diagnostic != nil {
|
|
return nil, cloneSourceInfos(sources), []Diagnostic{*diagnostic}
|
|
}
|
|
profilePath := filepath.Join(rootPath, "profiles", name+".yml")
|
|
canonical, err := canonicalExistingPath(profilePath)
|
|
if err != nil {
|
|
code := CodeProfileInvalid
|
|
message := "selected profile could not be resolved"
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
code, message = CodeProfileMissing, "selected profile does not exist"
|
|
} else if errors.Is(err, fs.ErrPermission) {
|
|
code, message = CodeImportPermission, "selected profile cannot be accessed"
|
|
}
|
|
return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(code, message, nil)}
|
|
}
|
|
if !containedPath(rootPath, canonical) {
|
|
return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileOutsideRoot, "selected profile resolves outside the configuration root", nil)}
|
|
}
|
|
for _, existing := range sources {
|
|
if existing.Path != "" && existing.Path == canonical {
|
|
return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileRepeated, "selected profile is already present in the base source graph", nil)}
|
|
}
|
|
}
|
|
if len(sources)+1 > limits.sources {
|
|
return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the source-count limit", nil)}
|
|
}
|
|
|
|
root, err := os.OpenRoot(rootPath)
|
|
if err != nil {
|
|
value := globFilesystemDiagnostic(err, "configuration root could not be opened for the selected profile", Location{})
|
|
value.Primary = nil
|
|
return nil, cloneSourceInfos(sources), []Diagnostic{value}
|
|
}
|
|
relative, err := filepath.Rel(rootPath, canonical)
|
|
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)}
|
|
}
|
|
|
|
sourceID := "source-" + strconv.Itoa(len(sources)+1)
|
|
profileLocation := &Location{SourceID: sourceID}
|
|
profileSource := SourceInfo{ID: sourceID, Label: filepath.ToSlash(filepath.Join("profiles", name+".yml")), Path: canonical, Layer: LayerProfile}
|
|
sources = append(cloneSourceInfos(sources), profileSource)
|
|
data, diagnostic := readProfileFile(root, relative, sourceID, limits.bytes-base.inputBytes)
|
|
if diagnostic != nil {
|
|
_ = root.Close()
|
|
return nil, sources, []Diagnostic{*diagnostic}
|
|
}
|
|
profileRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes})
|
|
if len(issues) != 0 {
|
|
_ = root.Close()
|
|
return nil, sources, parserDiagnostics(sourceID, issues)
|
|
}
|
|
nodeCount := countInputNodes(profileRoot)
|
|
if base.inputNodes+nodeCount > limits.nodes {
|
|
_ = root.Close()
|
|
return nil, sources, []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the aggregate node limit", profileLocation)}
|
|
}
|
|
if diagnostics := profileDirectiveDiagnostics(profileRoot, Path{}); len(diagnostics) != 0 {
|
|
_ = root.Close()
|
|
return nil, sources, diagnostics
|
|
}
|
|
if versionNode := fragmentVersionClaim(profileRoot, rule); versionNode != nil {
|
|
location := publicLocation(versionNode.Location)
|
|
_ = root.Close()
|
|
return nil, sources, []Diagnostic{{
|
|
Code: CodeProfileVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
|
Message: "profiles cannot contribute to or replace the schema-version path", Primary: &location,
|
|
}}
|
|
}
|
|
if err := root.Close(); err != nil {
|
|
return nil, sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected profile", nil)}
|
|
}
|
|
|
|
profiled, diagnostics := overlayDocuments(base, profileRoot)
|
|
if profiled != nil {
|
|
profiled.inputBytes = base.inputBytes + int64(len(data))
|
|
profiled.inputNodes = base.inputNodes + nodeCount
|
|
}
|
|
return profiled, sources, diagnostics
|
|
}
|
|
|
|
func readProfileFile(root *os.Root, relative, sourceID string, remaining int64) ([]byte, *Diagnostic) {
|
|
limit := min(int64(maxSourceBytes), remaining)
|
|
if limit < 0 {
|
|
limit = 0
|
|
}
|
|
preflight, err := root.Stat(relative)
|
|
if err != nil {
|
|
value := classifySourceError(err, sourceID)
|
|
return nil, &value
|
|
}
|
|
if !preflight.Mode().IsRegular() {
|
|
value := sourceDiagnostic(CodeSourceNotRegular, sourceID, "source is not a regular file")
|
|
return nil, &value
|
|
}
|
|
file, err := root.Open(relative)
|
|
if err != nil {
|
|
value := classifySourceError(err, sourceID)
|
|
return nil, &value
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
value := classifySourceError(err, sourceID)
|
|
return nil, &value
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
value := sourceDiagnostic(CodeSourceNotRegular, sourceID, "source is not a regular file")
|
|
return nil, &value
|
|
}
|
|
if info.Size() > limit {
|
|
value := importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds a source or aggregate byte limit", &Location{SourceID: sourceID})
|
|
return nil, &value
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(file, limit+1))
|
|
if err != nil {
|
|
value := classifySourceError(err, sourceID)
|
|
return nil, &value
|
|
}
|
|
if int64(len(data)) > limit {
|
|
value := importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds a source or aggregate byte limit", &Location{SourceID: sourceID})
|
|
return nil, &value
|
|
}
|
|
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
|
|
}
|