package configkit import ( "bytes" "errors" "io/fs" "os" "path/filepath" "strconv" "strings" 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} } rootClosed := false defer func() { if !rootClosed { _ = root.Close() } }() relative, err := filepath.Rel(rootPath, canonical) if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { 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 { return nil, sources, []Diagnostic{*diagnostic} } profileRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes}) if len(issues) != 0 { return nil, sources, parserDiagnostics(sourceID, issues) } nodeCount := countInputNodes(profileRoot) if base.inputNodes+nodeCount > limits.nodes { return nil, sources, []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the aggregate node limit", profileLocation)} } if diagnostics := profileDirectiveDiagnostics(profileRoot, Path{}); len(diagnostics) != 0 { return nil, sources, diagnostics } if versionNode := fragmentVersionClaim(profileRoot, rule); versionNode != nil { location := publicLocation(versionNode.Location) 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, }} } 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)} } 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 } data, failure := readRegularFile(relative, root.Stat, root.Open, limit) if failure != nil { value := sourceReadDiagnostic(failure, sourceID, CodeProfileLimitExceeded, "selected profile exceeds a source or aggregate byte limit") return nil, &value } return data, nil }