197 lines
7.7 KiB
Go
197 lines
7.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func validateFilesystemContext(source Source, context *FilesystemContext, main parsedSource) *Diagnostic {
|
|
if source.kind == sourceNone && context.FS == nil {
|
|
return validateAbsentHostFilesystemContext(context)
|
|
}
|
|
if nativeContext(source, context) {
|
|
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
|
|
if diagnostic != nil {
|
|
return diagnostic
|
|
}
|
|
return validateNativeDirectories(access, basePath, mainPath)
|
|
}
|
|
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 validateAbsentHostFilesystemContext(context *FilesystemContext) *Diagnostic {
|
|
rootPath, err := canonicalExistingPath(context.Root)
|
|
if err != nil {
|
|
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
|
|
return &value
|
|
}
|
|
basePath := rootPath
|
|
if context.BaseDir != "" {
|
|
basePath, err = canonicalExistingPath(context.BaseDir)
|
|
if err != nil {
|
|
value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil)
|
|
return &value
|
|
}
|
|
}
|
|
if !containedPath(rootPath, basePath) {
|
|
value := importDiagnostic(CodeImportOutsideRoot, "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
|
|
}
|
|
}
|
|
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 absentFilesystemOptionDiagnostics(context)
|
|
case sourceSearch:
|
|
if context.FS != nil {
|
|
return absentFilesystemOptionDiagnostics(context)
|
|
}
|
|
// A selected host file can derive its root; absence requires an
|
|
// explicit root. Check that compatibility only after selection.
|
|
if context.BaseDir != "" && !filepath.IsAbs(context.BaseDir) {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem base directory must be an absolute host path")}
|
|
}
|
|
case sourceFile:
|
|
if context.FS != nil {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "host file sources cannot use a native filesystem context")}
|
|
}
|
|
if context.BaseDir != "" {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")}
|
|
}
|
|
case sourceFilesystem:
|
|
if context.FS == nil || context.FS.namespace == nil || source.namespace == nil || context.FS.namespace.token != source.namespace.token {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem file context must use the source namespace")}
|
|
}
|
|
if context.BaseDir != "" {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")}
|
|
}
|
|
if context.Root != "" && !fs.ValidPath(context.Root) {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem root must be a slash-relative valid path")}
|
|
}
|
|
case sourceBytes, sourceReader, sourceMapping:
|
|
if context.BaseDir == "" || context.Root == "" {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem context requires base and root directories")}
|
|
}
|
|
if context.FS != nil {
|
|
if context.FS.namespace == nil || !fs.ValidPath(context.BaseDir) || !fs.ValidPath(context.Root) {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem base and root must be slash-relative valid paths")}
|
|
}
|
|
} else 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 absentFilesystemOptionDiagnostics(context *FilesystemContext) []Diagnostic {
|
|
if context.FS != nil {
|
|
if context.FS.namespace == nil || nilFilesystem(context.FS.namespace.filesystem) || context.BaseDir == "" || context.Root == "" || !fs.ValidPath(context.BaseDir) || !fs.ValidPath(context.Root) {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "native filesystem context requires a namespace, base, and root directory")}
|
|
}
|
|
} else if context.Root == "" || !filepath.IsAbs(context.Root) || context.BaseDir != "" && !filepath.IsAbs(context.BaseDir) {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "absent-input filesystem root and base directories must be absolute host paths")}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nativeContext(source Source, context *FilesystemContext) bool {
|
|
return source.kind == sourceFilesystem || context != nil && context.FS != nil
|
|
}
|
|
|
|
// nativeFilesystemAccess resolves namespace context without opening caller
|
|
// resources. File inputs always derive their base from the selected file.
|
|
func nativeFilesystemAccess(source Source, context *FilesystemContext) (*filesystemAccess, string, string, *Diagnostic) {
|
|
resolved, diagnostic := resolveNativeFilesystemContext(source, context)
|
|
if diagnostic != nil {
|
|
return nil, "", "", diagnostic
|
|
}
|
|
access, err := openResolvedFilesystemAccess(resolved)
|
|
if err != nil {
|
|
value := globFilesystemDiagnostic(err, "configuration root could not be opened", Location{})
|
|
value.Primary = nil
|
|
return nil, "", "", &value
|
|
}
|
|
return access, resolved.basePath, resolved.mainPath, nil
|
|
}
|
|
|
|
func validateNativeDirectories(access *filesystemAccess, basePath, mainPath string) *Diagnostic {
|
|
if !access.contains(basePath) || mainPath != "" && !access.contains(mainPath) {
|
|
value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)
|
|
return &value
|
|
}
|
|
for _, directory := range []string{access.rootPath, basePath} {
|
|
info, err := access.stat(directory)
|
|
if err != nil || !info.IsDir() {
|
|
value := importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil)
|
|
return &value
|
|
}
|
|
}
|
|
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
|
|
}
|
|
resolved, diagnostic := resolveHostFilesystemContext(source, context, main, nil)
|
|
if diagnostic != nil {
|
|
return "", "", "", diagnostic
|
|
}
|
|
if diagnostic := validateResolvedFilesystemDirectories(resolved); diagnostic != nil {
|
|
return "", "", "", diagnostic
|
|
}
|
|
return resolved.rootPath, resolved.basePath, resolved.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))
|
|
}
|