1package fs
2
3import (
4	"bytes"
5	"context"
6	"io"
7	"os"
8	"path/filepath"
9	"strings"
10
11	"github.com/pkg/errors"
12)
13
14var (
15	errTooManyLinks = errors.New("too many links")
16)
17
18type currentPath struct {
19	path     string
20	f        os.FileInfo
21	fullPath string
22}
23
24func pathChange(lower, upper *currentPath) (ChangeKind, string) {
25	if lower == nil {
26		if upper == nil {
27			panic("cannot compare nil paths")
28		}
29		return ChangeKindAdd, upper.path
30	}
31	if upper == nil {
32		return ChangeKindDelete, lower.path
33	}
34	// TODO: compare by directory
35
36	switch i := strings.Compare(lower.path, upper.path); {
37	case i < 0:
38		// File in lower that is not in upper
39		return ChangeKindDelete, lower.path
40	case i > 0:
41		// File in upper that is not in lower
42		return ChangeKindAdd, upper.path
43	default:
44		return ChangeKindModify, upper.path
45	}
46}
47
48func sameFile(f1, f2 *currentPath) (bool, error) {
49	if os.SameFile(f1.f, f2.f) {
50		return true, nil
51	}
52
53	equalStat, err := compareSysStat(f1.f.Sys(), f2.f.Sys())
54	if err != nil || !equalStat {
55		return equalStat, err
56	}
57
58	if eq, err := compareCapabilities(f1.fullPath, f2.fullPath); err != nil || !eq {
59		return eq, err
60	}
61
62	// If not a directory also check size, modtime, and content
63	if !f1.f.IsDir() {
64		if f1.f.Size() != f2.f.Size() {
65			return false, nil
66		}
67		t1 := f1.f.ModTime()
68		t2 := f2.f.ModTime()
69
70		if t1.Unix() != t2.Unix() {
71			return false, nil
72		}
73
74		// If the timestamp may have been truncated in both of the
75		// files, check content of file to determine difference
76		if t1.Nanosecond() == 0 && t2.Nanosecond() == 0 {
77			var eq bool
78			if (f1.f.Mode() & os.ModeSymlink) == os.ModeSymlink {
79				eq, err = compareSymlinkTarget(f1.fullPath, f2.fullPath)
80			} else if f1.f.Size() > 0 {
81				eq, err = compareFileContent(f1.fullPath, f2.fullPath)
82			}
83			if err != nil || !eq {
84				return eq, err
85			}
86		} else if t1.Nanosecond() != t2.Nanosecond() {
87			return false, nil
88		}
89	}
90
91	return true, nil
92}
93
94func compareSymlinkTarget(p1, p2 string) (bool, error) {
95	t1, err := os.Readlink(p1)
96	if err != nil {
97		return false, err
98	}
99	t2, err := os.Readlink(p2)
100	if err != nil {
101		return false, err
102	}
103	return t1 == t2, nil
104}
105
106const compareChuckSize = 32 * 1024
107
108// compareFileContent compares the content of 2 same sized files
109// by comparing each byte.
110func compareFileContent(p1, p2 string) (bool, error) {
111	f1, err := os.Open(p1)
112	if err != nil {
113		return false, err
114	}
115	defer f1.Close()
116	f2, err := os.Open(p2)
117	if err != nil {
118		return false, err
119	}
120	defer f2.Close()
121
122	b1 := make([]byte, compareChuckSize)
123	b2 := make([]byte, compareChuckSize)
124	for {
125		n1, err1 := f1.Read(b1)
126		if err1 != nil && err1 != io.EOF {
127			return false, err1
128		}
129		n2, err2 := f2.Read(b2)
130		if err2 != nil && err2 != io.EOF {
131			return false, err2
132		}
133		if n1 != n2 || !bytes.Equal(b1[:n1], b2[:n2]) {
134			return false, nil
135		}
136		if err1 == io.EOF && err2 == io.EOF {
137			return true, nil
138		}
139	}
140}
141
142func pathWalk(ctx context.Context, root string, pathC chan<- *currentPath) error {
143	return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
144		if err != nil {
145			return err
146		}
147
148		// Rebase path
149		path, err = filepath.Rel(root, path)
150		if err != nil {
151			return err
152		}
153
154		path = filepath.Join(string(os.PathSeparator), path)
155
156		// Skip root
157		if path == string(os.PathSeparator) {
158			return nil
159		}
160
161		p := &currentPath{
162			path:     path,
163			f:        f,
164			fullPath: filepath.Join(root, path),
165		}
166
167		select {
168		case <-ctx.Done():
169			return ctx.Err()
170		case pathC <- p:
171			return nil
172		}
173	})
174}
175
176func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, error) {
177	select {
178	case <-ctx.Done():
179		return nil, ctx.Err()
180	case p := <-pathC:
181		return p, nil
182	}
183}
184
185// RootPath joins a path with a root, evaluating and bounding any
186// symlink to the root directory.
187func RootPath(root, path string) (string, error) {
188	if path == "" {
189		return root, nil
190	}
191	var linksWalked int // to protect against cycles
192	for {
193		i := linksWalked
194		newpath, err := walkLinks(root, path, &linksWalked)
195		if err != nil {
196			return "", err
197		}
198		path = newpath
199		if i == linksWalked {
200			newpath = filepath.Join("/", newpath)
201			if path == newpath {
202				return filepath.Join(root, newpath), nil
203			}
204			path = newpath
205		}
206	}
207}
208
209func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
210	if *linksWalked > 255 {
211		return "", false, errTooManyLinks
212	}
213
214	path = filepath.Join("/", path)
215	if path == "/" {
216		return path, false, nil
217	}
218	realPath := filepath.Join(root, path)
219
220	fi, err := os.Lstat(realPath)
221	if err != nil {
222		// If path does not yet exist, treat as non-symlink
223		if os.IsNotExist(err) {
224			return path, false, nil
225		}
226		return "", false, err
227	}
228	if fi.Mode()&os.ModeSymlink == 0 {
229		return path, false, nil
230	}
231	newpath, err = os.Readlink(realPath)
232	if err != nil {
233		return "", false, err
234	}
235	if filepath.IsAbs(newpath) && strings.HasPrefix(newpath, root) {
236		newpath = newpath[:len(root)]
237		if !strings.HasPrefix(newpath, "/") {
238			newpath = "/" + newpath
239		}
240	}
241	*linksWalked++
242	return newpath, true, nil
243}
244
245func walkLinks(root, path string, linksWalked *int) (string, error) {
246	switch dir, file := filepath.Split(path); {
247	case dir == "":
248		newpath, _, err := walkLink(root, file, linksWalked)
249		return newpath, err
250	case file == "":
251		if os.IsPathSeparator(dir[len(dir)-1]) {
252			if dir == "/" {
253				return dir, nil
254			}
255			return walkLinks(root, dir[:len(dir)-1], linksWalked)
256		}
257		newpath, _, err := walkLink(root, dir, linksWalked)
258		return newpath, err
259	default:
260		newdir, err := walkLinks(root, dir, linksWalked)
261		if err != nil {
262			return "", err
263		}
264		newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
265		if err != nil {
266			return "", err
267		}
268		if !islink {
269			return newpath, nil
270		}
271		if filepath.IsAbs(newpath) {
272			return newpath, nil
273		}
274		return filepath.Join(newdir, newpath), nil
275	}
276}
277