package filecatalog import ( "context" "fmt" "io/fs" "os" "path" "path/filepath" "sort" "strings" ) // FindYAMLFiles returns sorted full paths for .yaml and .yml files under root. func FindYAMLFiles(ctx context.Context, root string) ([]string, error) { var files []string err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() default: } if d.IsDir() { return nil } if !IsYAMLFile(d.Name()) { return nil } files = append(files, path) return nil }) sort.Strings(files) return files, err } // FindFSYAMLFiles returns sorted paths for .yaml and .yml files under root in fsys. func FindFSYAMLFiles(ctx context.Context, fsys fs.FS, root string) ([]string, error) { cleanRoot := CleanFSRoot(root) var files []string err := fs.WalkDir(fsys, cleanRoot, func(name string, d fs.DirEntry, err error) error { if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() default: } if d.IsDir() { return nil } if !IsYAMLFile(d.Name()) { return nil } files = append(files, name) return nil }) sort.Strings(files) return files, err } // RelativePath computes a clean relative path from root to path. func RelativePath(root string, filePath string) string { rel, err := filepath.Rel(root, filePath) if err != nil { return filepath.Clean(filePath) } return filepath.Clean(rel) } // CleanFSRoot normalizes a root path for use with fs.FS. func CleanFSRoot(root string) string { root = strings.TrimSpace(root) if root == "" || root == "." { return "." } return path.Clean(root) } // DisplayPath returns name relative to root for messages about fs.FS paths. func DisplayPath(root string, name string) string { cleanRoot := CleanFSRoot(root) cleanName := path.Clean(name) if cleanRoot == "." { return cleanName } prefix := strings.TrimSuffix(cleanRoot, "/") + "/" if strings.HasPrefix(cleanName, prefix) { return strings.TrimPrefix(cleanName, prefix) } return cleanName } // ResolveFSPath resolves userPath from baseDir and keeps it inside root. func ResolveFSPath(root string, baseDir string, userPath string) (string, string, error) { cleanRoot := CleanFSRoot(root) cleanBase := path.Clean(strings.TrimSpace(baseDir)) if cleanBase == "" { cleanBase = cleanRoot } if !containsFSPath(cleanRoot, cleanBase) { return "", "", fmt.Errorf("base path %q is outside source root %q", cleanBase, cleanRoot) } cleanUserPath := strings.TrimSpace(userPath) if cleanUserPath == "" { return "", "", fmt.Errorf("path is required") } cleanUserPath = path.Clean(cleanUserPath) if path.IsAbs(cleanUserPath) { return "", "", fmt.Errorf("path %q must be relative", userPath) } resolved := path.Clean(path.Join(cleanBase, cleanUserPath)) if !containsFSPath(cleanRoot, resolved) { return "", "", fmt.Errorf("path %q escapes source root %q", userPath, cleanRoot) } return resolved, DisplayPath(cleanRoot, resolved), nil } func containsFSPath(root string, name string) bool { root = CleanFSRoot(root) name = path.Clean(name) if root == "." { return name == "." || (name != ".." && !strings.HasPrefix(name, "../")) } return name == root || strings.HasPrefix(name, strings.TrimSuffix(root, "/")+"/") } // Stem strips .yaml or .yml from a file name. func Stem(name string) string { name = strings.TrimSuffix(name, ".yaml") name = strings.TrimSuffix(name, ".yml") return name } func IsYAMLFile(name string) bool { return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") }