mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
packaged installs put the binary in /usr/bin and the modules under /usr/share/sif/modules, so the loader found zero built-in modules with no error. add the freedesktop data dirs ($XDG_DATA_DIRS, default /usr/local/share and /usr/share) as fallback search paths after the existing executable-relative and working-dir locations, first-existing wins. the new IsDir check also makes resolution stricter: a plain file named modules no longer counts as the directory.
244 lines
7.2 KiB
Go
244 lines
7.2 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package modules
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/vmfunc/sif/internal/output"
|
|
)
|
|
|
|
// builtinFS holds the modules embedded into the binary. it's set once, from the
|
|
// repo-root package that can actually run the go:embed directive (go:embed can't
|
|
// reach a parent directory, and modules/ sits above this package). it stays nil
|
|
// in builds and tests that don't import that package, so the loader simply falls
|
|
// back to the filesystem as before.
|
|
var builtinFS fs.FS
|
|
|
|
// SetBuiltinFS registers the embedded module filesystem. see builtinFS.
|
|
func SetBuiltinFS(fsys fs.FS) { builtinFS = fsys }
|
|
|
|
// Loader handles module discovery and loading.
|
|
type Loader struct {
|
|
builtinDir string
|
|
userDir string
|
|
embedded fs.FS
|
|
loaded int
|
|
}
|
|
|
|
// NewLoader creates a new module loader.
|
|
// It automatically detects the built-in modules directory and sets up
|
|
// the user modules directory based on the operating system.
|
|
func NewLoader() (*Loader, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get home dir: %w", err)
|
|
}
|
|
|
|
builtinDir := resolveBuiltinDir()
|
|
|
|
// User modules directory based on OS
|
|
var userDir string
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
userDir = filepath.Join(home, "AppData", "Local", "sif", "modules")
|
|
default:
|
|
userDir = filepath.Join(home, ".config", "sif", "modules")
|
|
}
|
|
|
|
return &Loader{
|
|
builtinDir: builtinDir,
|
|
userDir: userDir,
|
|
embedded: builtinFS,
|
|
}, nil
|
|
}
|
|
|
|
// resolveBuiltinDir picks the built-in modules directory: the first existing
|
|
// candidate, or the working-directory default when none are present (LoadAll
|
|
// then logs "no built-in modules found" as before).
|
|
func resolveBuiltinDir() string {
|
|
if dir := firstExistingDir(builtinDirCandidates()); dir != "" {
|
|
return dir
|
|
}
|
|
return "modules"
|
|
}
|
|
|
|
// builtinDirCandidates lists the directories to probe for built-in modules,
|
|
// most specific first: next to the executable, the working directory (for
|
|
// development), then the freedesktop system data dirs so packaged installs
|
|
// (modules under /usr/share/sif) are found too.
|
|
func builtinDirCandidates() []string {
|
|
candidates := make([]string, 0, 4)
|
|
|
|
if execPath, err := os.Executable(); err == nil {
|
|
candidates = append(candidates, filepath.Join(filepath.Dir(execPath), "modules"))
|
|
}
|
|
candidates = append(candidates, "modules")
|
|
|
|
for _, dir := range dataDirs() {
|
|
candidates = append(candidates, filepath.Join(dir, "sif", "modules"))
|
|
}
|
|
|
|
return candidates
|
|
}
|
|
|
|
// dataDirs returns the freedesktop base data directories, honoring
|
|
// $XDG_DATA_DIRS and falling back to the spec default when it is unset.
|
|
func dataDirs() []string {
|
|
if env := os.Getenv("XDG_DATA_DIRS"); env != "" {
|
|
return filepath.SplitList(env)
|
|
}
|
|
return []string{"/usr/local/share", "/usr/share"}
|
|
}
|
|
|
|
func firstExistingDir(candidates []string) string {
|
|
for _, dir := range candidates {
|
|
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
|
return dir
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// LoadAll discovers and loads all modules from both built-in
|
|
// and user directories.
|
|
func (l *Loader) LoadAll() error {
|
|
// Load built-in modules first, preferring an on-disk modules/ dir (dev tree
|
|
// or a release that ships the folder alongside the binary).
|
|
before := l.loaded
|
|
if err := l.loadDir(l.builtinDir, false); err != nil {
|
|
log.Debugf("No built-in modules found: %v", err)
|
|
}
|
|
|
|
// nothing on disk: fall back to the modules embedded in the binary so a bare
|
|
// `go install`ed sif still ships its built-in modules.
|
|
if l.loaded == before && l.embedded != nil {
|
|
if err := l.loadFS(l.embedded); err != nil {
|
|
log.Debugf("No embedded modules loaded: %v", err)
|
|
}
|
|
}
|
|
|
|
// Load user modules (can override built-in)
|
|
if err := l.loadDir(l.userDir, true); err != nil {
|
|
// User dir might not exist, that's OK
|
|
if !os.IsNotExist(err) {
|
|
log.Debugf("No user modules found: %v", err)
|
|
}
|
|
}
|
|
|
|
if l.loaded > 0 {
|
|
modLog := output.Module("MODULES")
|
|
modLog.Info("Loaded %d modules", l.loaded)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadDir loads modules from a directory.
|
|
func (l *Loader) loadDir(dir string, userDefined bool) error {
|
|
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
switch filepath.Ext(path) {
|
|
case ".yaml", ".yml":
|
|
if err := l.loadYAML(path); err != nil {
|
|
log.Warnf("Failed to load module %s: %v", path, err)
|
|
} else {
|
|
l.loaded++
|
|
}
|
|
case ".go":
|
|
if err := l.loadScript(path); err != nil {
|
|
log.Debugf("Failed to load script %s: %v", path, err)
|
|
} else {
|
|
l.loaded++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// loadFS loads yaml modules from an embedded filesystem. only yaml is embedded
|
|
// (the .go script path is a filesystem-only dev affordance), so this walks for
|
|
// yaml files and parses them from bytes.
|
|
func (l *Loader) loadFS(fsys fs.FS) error {
|
|
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
switch filepath.Ext(path) {
|
|
case ".yaml", ".yml":
|
|
data, rerr := fs.ReadFile(fsys, path)
|
|
if rerr != nil {
|
|
log.Warnf("Failed to read embedded module %s: %v", path, rerr)
|
|
return nil
|
|
}
|
|
def, perr := ParseYAMLModuleBytes(data)
|
|
if perr != nil {
|
|
log.Warnf("Failed to load embedded module %s: %v", path, perr)
|
|
return nil
|
|
}
|
|
Register(newYAMLModuleWrapper(def, path))
|
|
l.loaded++
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// loadYAML loads a YAML module definition.
|
|
func (l *Loader) loadYAML(path string) error {
|
|
def, err := ParseYAMLModule(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
module := newYAMLModuleWrapper(def, path)
|
|
Register(module)
|
|
return nil
|
|
}
|
|
|
|
// loadScript loads a Go script module.
|
|
// Implementation will be provided in script.go.
|
|
func (l *Loader) loadScript(path string) error {
|
|
// Will be implemented in script.go
|
|
return nil
|
|
}
|
|
|
|
// BuiltinDir returns the built-in modules directory path.
|
|
func (l *Loader) BuiltinDir() string {
|
|
return l.builtinDir
|
|
}
|
|
|
|
// UserDir returns the user modules directory path.
|
|
func (l *Loader) UserDir() string {
|
|
return l.userDir
|
|
}
|
|
|
|
// Loaded returns the number of loaded modules.
|
|
func (l *Loader) Loaded() int {
|
|
return l.loaded
|
|
}
|