1package fsutil
2
3import (
4	"bufio"
5	"fmt"
6	"io"
7	"math/rand"
8	"os"
9	"os/user"
10	"path/filepath"
11	"regexp"
12	"strings"
13	"time"
14
15	"github.com/gopasspw/gopass/pkg/debug"
16)
17
18var reCleanFilename = regexp.MustCompile(`[^\w\d@.-]`)
19
20// CleanFilename strips all possibly suspicious characters from a filename
21// WARNING: NOT suiteable for pathnames as slashes will be stripped as well!
22func CleanFilename(in string) string {
23	return strings.Trim(reCleanFilename.ReplaceAllString(in, "_"), "_ ")
24}
25
26// CleanPath resolves common aliases in a path and cleans it as much as possible
27func CleanPath(path string) string {
28	// http://stackoverflow.com/questions/17609732/expand-tilde-to-home-directory
29	// TODO: We should consider if we really want to rewrite ~
30	if len(path) > 1 && path[:2] == "~/" {
31		usr, _ := user.Current()
32		dir := usr.HomeDir
33		if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" {
34			dir = hd
35		}
36		path = strings.Replace(path, "~/", dir+"/", 1)
37	}
38	if p, err := filepath.Abs(path); err == nil {
39		return p
40	}
41	return filepath.Clean(path)
42}
43
44// IsDir checks if a certain path exists and is a directory
45// https://stackoverflow.com/questions/10510691/how-to-check-whether-a-file-or-directory-denoted-by-a-path-exists-in-golang
46func IsDir(path string) bool {
47	fi, err := os.Stat(path)
48	if err != nil {
49		if os.IsNotExist(err) {
50			// not found
51			return false
52		}
53		debug.Log("failed to check dir %s: %s\n", path, err)
54		return false
55	}
56
57	return fi.IsDir()
58}
59
60// IsFile checks if a certain path is actually a file
61func IsFile(path string) bool {
62	fi, err := os.Stat(path)
63	if err != nil {
64		if os.IsNotExist(err) {
65			// not found
66			return false
67		}
68		debug.Log("failed to check file %s: %s\n", path, err)
69		return false
70	}
71
72	return fi.Mode().IsRegular()
73}
74
75// IsEmptyDir checks if a certain path is an empty directory
76func IsEmptyDir(path string) (bool, error) {
77	empty := true
78	if err := filepath.Walk(path, func(fp string, fi os.FileInfo, ferr error) error {
79		if ferr != nil {
80			return ferr
81		}
82		if fi.IsDir() && (fi.Name() == "." || fi.Name() == "..") {
83			return filepath.SkipDir
84		}
85		if !fi.IsDir() {
86			empty = false
87		}
88		return nil
89	}); err != nil {
90		return false, err
91	}
92	return empty, nil
93}
94
95// Shred overwrite the given file any number of times
96func Shred(path string, runs int) error {
97	rand.Seed(time.Now().UnixNano())
98	fh, err := os.OpenFile(path, os.O_WRONLY, 0600)
99	if err != nil {
100		return fmt.Errorf("failed to open file %q: %w", path, err)
101	}
102	buf := make([]byte, 1024)
103	for i := 0; i < runs; i++ {
104		// overwrite using pseudo-random data n-1 times and
105		// use zeros in the last iteration
106		if i < runs-1 {
107			_, _ = rand.Read(buf)
108		} else {
109			buf = make([]byte, 1024)
110		}
111		if _, err := fh.Seek(0, 0); err != nil {
112			return fmt.Errorf("failed to seek to 0,0: %w", err)
113		}
114		if _, err := fh.Write(buf); err != nil {
115			if err != io.EOF {
116				return fmt.Errorf("failed to write to file: %w", err)
117			}
118		}
119		// if we fail to sync the written blocks to disk it'd be pointless
120		// do any further loops
121		if err := fh.Sync(); err != nil {
122			return fmt.Errorf("failed to sync to disk: %w", err)
123		}
124	}
125	if err := fh.Close(); err != nil {
126		return fmt.Errorf("failed to close file after writing: %w", err)
127	}
128
129	return os.Remove(path)
130}
131
132// FileContains searches the given file for the search string and returns true
133// iff it's an exact (substring) match.
134func FileContains(path, needle string) bool {
135	fh, err := os.Open(path)
136	if err != nil {
137		debug.Log("failed to open %q for reading: %s", path, err)
138		return false
139	}
140	defer fh.Close()
141
142	s := bufio.NewScanner(fh)
143	for s.Scan() {
144		if strings.Contains(s.Text(), needle) {
145			return true
146		}
147	}
148	return false
149}
150