diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c657e55..1991ac2 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -7,7 +7,6 @@ on: - '**.mod' workflow_dispatch: - jobs: build: name: Test Builds @@ -27,7 +26,8 @@ jobs: uses: actions/checkout@v3 - name: Test - run: go test ./... + run: go test -race ./... - - name: Build - run: go build . \ No newline at end of file + - name: Run Example + run: go run . + working-directory: examples/basic \ No newline at end of file diff --git a/goflags.go b/goflags.go index 75401ad..2ef2c18 100644 --- a/goflags.go +++ b/goflags.go @@ -7,7 +7,6 @@ import ( "io" "os" "path" - "path/filepath" "reflect" "strconv" "strings" @@ -16,6 +15,7 @@ import ( "github.com/cnf/structhash" fileutil "github.com/projectdiscovery/utils/file" + folderutil "github.com/projectdiscovery/utils/folder" permissionutil "github.com/projectdiscovery/utils/permission" "golang.org/x/exp/maps" "gopkg.in/yaml.v3" @@ -106,20 +106,40 @@ func (flagSet *FlagSet) Parse() error { flagSet.CommandLine.SetOutput(os.Stdout) flagSet.CommandLine.Usage = flagSet.usageFunc _ = flagSet.CommandLine.Parse(os.Args[1:]) + configFilePath, _ := flagSet.GetConfigFilePath() - configFilePath, err := flagSet.GetConfigFilePath() - if err != nil { - return err - } - _ = os.MkdirAll(filepath.Dir(configFilePath), permissionutil.ConfigFolderPermission) + // migrate data from old config dir to new one + // Ref: https://github.com/projectdiscovery/nuclei/issues/3576 + flagSet.migrateConfigDir() + + // if config file doesn't exist, create one if !fileutil.FileExists(configFilePath) { configData := flagSet.generateDefaultConfig() + configFileDir := flagSet.GetToolConfigDir() + if !fileutil.FolderExists(configFileDir) { + _ = fileutil.CreateFolder(configFileDir) + } return os.WriteFile(configFilePath, configData, permissionutil.ConfigFilePermission) } + _ = flagSet.MergeConfigFile(configFilePath) // try to read default config after parsing flags return nil } +func (flagSet *FlagSet) migrateConfigDir() { + // migration condition + // 1. old config dir exists + // 2. new config dir doesn't exist + // 3. old config dir is not same as new config dir + + toolConfigDir := flagSet.GetToolConfigDir() + if toolConfigDir != oldAppConfigDir && fileutil.FolderExists(oldAppConfigDir) && !fileutil.FolderExists(toolConfigDir) { + _ = fileutil.CreateFolder(toolConfigDir) + // move old config dir to new one + _ = folderutil.SyncDirectory(oldAppConfigDir, toolConfigDir) + } +} + // generateDefaultConfig generates a default YAML config file for a flagset. func (flagSet *FlagSet) generateDefaultConfig() []byte { hashes := make(map[string]struct{}) diff --git a/goflags_test.go b/goflags_test.go index 85ed9bc..79e5b25 100644 --- a/goflags_test.go +++ b/goflags_test.go @@ -5,12 +5,15 @@ import ( "flag" "fmt" "os" + "path/filepath" "reflect" "strconv" "strings" "testing" "time" + fileutil "github.com/projectdiscovery/utils/file" + osutil "github.com/projectdiscovery/utils/os" permissionutil "github.com/projectdiscovery/utils/permission" "github.com/stretchr/testify/assert" @@ -418,3 +421,69 @@ func tearDown(uniqueValue string) { flag.CommandLine = flag.NewFlagSet(uniqueValue, flag.ContinueOnError) flag.CommandLine.Usage = flag.Usage } + +func TestConfigDirMigration(t *testing.T) { + // remove any args added by previous test + os.Args = []string{ + os.Args[0], + } + // setup test old config dir + createEmptyFilesinDir(t, oldAppConfigDir) + + flagset := NewFlagSet() + flagset.CommandLine = flag.NewFlagSet("goflags", flag.ContinueOnError) + newToolCfgDir := flagset.GetToolConfigDir() + + // cleanup and debug + defer func() { + // cleanup + if t.Failed() { + t.Logf("old config dir: %s", oldAppConfigDir) + t.Logf("new config dir: %s", newToolCfgDir) + cfgFiles, _ := os.ReadDir(oldAppConfigDir) + for _, cfgFile := range cfgFiles { + t.Logf("found config file in old dir : %s", cfgFile.Name()) + } + cfgFiles, _ = os.ReadDir(newToolCfgDir) + for _, cfgFile := range cfgFiles { + t.Logf("found config file in new dir : %s", cfgFile.Name()) + } + } + _ = os.RemoveAll(oldAppConfigDir) + _ = os.RemoveAll(newToolCfgDir) + }() + + // remove new config dir if it already exists from previous test + _ = os.RemoveAll(newToolCfgDir) + + // create test flag and parse it + var testflag string + flagset.CreateGroup("Example", "Example", + flagset.StringVarP(&testflag, "test", "t", "", "test flag"), + ) + + if err := flagset.Parse(); err != nil { + require.Nil(t, err, "could not parse flags") + } + + // oldAppConfigDir and newConfigDir is same in case of linux (unless sandbox or something else is used) + // migration will only happen on windows & macOS (darwin) Ref: https://pkg.go.dev/os#UserConfigDir + if oldAppConfigDir != newToolCfgDir || !osutil.IsLinux() { + // check if config files are moved to new config dir + require.FileExistsf(t, filepath.Join(newToolCfgDir, "config.yaml"), "config file not created in new config dir") + require.FileExistsf(t, filepath.Join(newToolCfgDir, "provider-config.yaml"), "config file not created in new config dir") + } + + tearDown(t.Name()) +} + +func createEmptyFilesinDir(t *testing.T, dirname string) { + if !fileutil.FolderExists(dirname) { + _ = fileutil.CreateFolder(dirname) + } + // create empty yaml config files + err := os.WriteFile(filepath.Join(oldAppConfigDir, "config.yaml"), []byte{}, os.ModePerm) + require.Nil(t, err, "could not create old config file") + err = os.WriteFile(filepath.Join(oldAppConfigDir, "provider-config.yaml"), []byte{}, os.ModePerm) + require.Nil(t, err, "could not create old config file") +} diff --git a/path.go b/path.go index 27a4e52..d673ed3 100644 --- a/path.go +++ b/path.go @@ -4,23 +4,25 @@ import ( "os" "path/filepath" "strings" + + folderutil "github.com/projectdiscovery/utils/folder" ) +var oldAppConfigDir = filepath.Join(folderutil.HomeDirOrDefault("."), ".config", getToolName()) + // GetConfigFilePath returns the config file path func (flagSet *FlagSet) GetConfigFilePath() (string, error) { // return configFilePath if already set if flagSet.configFilePath != "" { return flagSet.configFilePath, nil } - // generate default config name - appName := filepath.Base(os.Args[0]) - // trim extension from app name - appName = strings.TrimSuffix(appName, filepath.Ext(appName)) - homePath, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(homePath, ".config", appName, "config.yaml"), nil + return filepath.Join(folderutil.AppConfigDirOrDefault(".", getToolName()), "config.yaml"), nil +} + +// GetToolConfigDir returns the config dir path of the tool +func (flagset *FlagSet) GetToolConfigDir() string { + cfgFilePath, _ := flagset.GetConfigFilePath() + return filepath.Dir(cfgFilePath) } // SetConfigFilePath sets custom config file path @@ -28,16 +30,8 @@ func (flagSet *FlagSet) SetConfigFilePath(filePath string) { flagSet.configFilePath = filePath } -// Deprecated: Use FlagSet.GetConfigFilePath instead. -// GetConfigFilePath returns the default config file path -func GetConfigFilePath() (string, error) { +// getToolName returns the name of the tool +func getToolName() string { appName := filepath.Base(os.Args[0]) - // trim extension from app name - appName = strings.TrimSuffix(appName, filepath.Ext(appName)) - homePath, err := os.UserHomeDir() - if err != nil { - return "", err - } - - return filepath.Join(homePath, ".config", appName, "config.yaml"), nil + return strings.TrimSuffix(appName, filepath.Ext(appName)) }