| /* |
| Copyright The containerd Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package fs |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "io" |
| "os" |
| "path/filepath" |
| ) |
| |
| var errTooManyLinks = errors.New("too many links") |
| |
| type currentPath struct { |
| path string |
| f os.FileInfo |
| fullPath string |
| } |
| |
| func pathChange(lower, upper *currentPath) (ChangeKind, string) { |
| if lower == nil { |
| if upper == nil { |
| panic("cannot compare nil paths") |
| } |
| return ChangeKindAdd, upper.path |
| } |
| if upper == nil { |
| return ChangeKindDelete, lower.path |
| } |
| |
| switch i := directoryCompare(lower.path, upper.path); { |
| case i < 0: |
| // File in lower that is not in upper |
| return ChangeKindDelete, lower.path |
| case i > 0: |
| // File in upper that is not in lower |
| return ChangeKindAdd, upper.path |
| default: |
| return ChangeKindModify, upper.path |
| } |
| } |
| |
| func directoryCompare(a, b string) int { |
| l := len(a) |
| if len(b) < l { |
| l = len(b) |
| } |
| for i := 0; i < l; i++ { |
| c1, c2 := a[i], b[i] |
| if c1 == filepath.Separator { |
| c1 = byte(0) |
| } |
| if c2 == filepath.Separator { |
| c2 = byte(0) |
| } |
| if c1 < c2 { |
| return -1 |
| } |
| if c1 > c2 { |
| return +1 |
| } |
| } |
| if len(a) < len(b) { |
| return -1 |
| } |
| if len(a) > len(b) { |
| return +1 |
| } |
| return 0 |
| } |
| |
| func sameFile(f1, f2 *currentPath) (bool, error) { |
| if os.SameFile(f1.f, f2.f) { |
| return true, nil |
| } |
| |
| equalStat, err := compareSysStat(f1.f.Sys(), f2.f.Sys()) |
| if err != nil || !equalStat { |
| return equalStat, err |
| } |
| |
| if eq, err := compareCapabilities(f1.fullPath, f2.fullPath); err != nil || !eq { |
| return eq, err |
| } |
| |
| // If not a directory also check size, modtime, and content |
| if !f1.f.IsDir() { |
| if f1.f.Size() != f2.f.Size() { |
| return false, nil |
| } |
| t1 := f1.f.ModTime() |
| t2 := f2.f.ModTime() |
| |
| if t1.Unix() != t2.Unix() { |
| return false, nil |
| } |
| |
| // If the timestamp may have been truncated in both of the |
| // files, check content of file to determine difference |
| if t1.Nanosecond() == 0 && t2.Nanosecond() == 0 { |
| if (f1.f.Mode() & os.ModeSymlink) == os.ModeSymlink { |
| return compareSymlinkTarget(f1.fullPath, f2.fullPath) |
| } |
| if f1.f.Size() == 0 { // if file sizes are zero length, the files are the same by definition |
| return true, nil |
| } |
| return compareFileContent(f1.fullPath, f2.fullPath) |
| } else if t1.Nanosecond() != t2.Nanosecond() { |
| return false, nil |
| } |
| } |
| |
| return true, nil |
| } |
| |
| func compareSymlinkTarget(p1, p2 string) (bool, error) { |
| t1, err := os.Readlink(p1) |
| if err != nil { |
| return false, err |
| } |
| t2, err := os.Readlink(p2) |
| if err != nil { |
| return false, err |
| } |
| return t1 == t2, nil |
| } |
| |
| const compareChuckSize = 32 * 1024 |
| |
| // compareFileContent compares the content of 2 same sized files |
| // by comparing each byte. |
| func compareFileContent(p1, p2 string) (bool, error) { |
| f1, err := os.Open(p1) |
| if err != nil { |
| return false, err |
| } |
| defer f1.Close() |
| f2, err := os.Open(p2) |
| if err != nil { |
| return false, err |
| } |
| defer f2.Close() |
| |
| b1 := make([]byte, compareChuckSize) |
| b2 := make([]byte, compareChuckSize) |
| for { |
| n1, err1 := f1.Read(b1) |
| if err1 != nil && err1 != io.EOF { |
| return false, err1 |
| } |
| n2, err2 := f2.Read(b2) |
| if err2 != nil && err2 != io.EOF { |
| return false, err2 |
| } |
| if n1 != n2 || !bytes.Equal(b1[:n1], b2[:n2]) { |
| return false, nil |
| } |
| if err1 == io.EOF && err2 == io.EOF { |
| return true, nil |
| } |
| } |
| } |
| |
| func pathWalk(ctx context.Context, root string, pathC chan<- *currentPath) error { |
| return filepath.Walk(root, func(path string, f os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| |
| // Rebase path |
| path, err = filepath.Rel(root, path) |
| if err != nil { |
| return err |
| } |
| |
| path = filepath.Join(string(os.PathSeparator), path) |
| |
| // Skip root |
| if path == string(os.PathSeparator) { |
| return nil |
| } |
| |
| p := ¤tPath{ |
| path: path, |
| f: f, |
| fullPath: filepath.Join(root, path), |
| } |
| |
| select { |
| case <-ctx.Done(): |
| return ctx.Err() |
| case pathC <- p: |
| return nil |
| } |
| }) |
| } |
| |
| func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, error) { |
| select { |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| case p := <-pathC: |
| return p, nil |
| } |
| } |
| |
| // RootPath joins a path with a root, evaluating and bounding any |
| // symlink to the root directory. |
| func RootPath(root, path string) (string, error) { |
| if path == "" { |
| return root, nil |
| } |
| var linksWalked int // to protect against cycles |
| for { |
| i := linksWalked |
| newpath, err := walkLinks(root, path, &linksWalked) |
| if err != nil { |
| return "", err |
| } |
| path = newpath |
| if i == linksWalked { |
| newpath = filepath.Join("/", newpath) |
| if path == newpath { |
| return filepath.Join(root, newpath), nil |
| } |
| path = newpath |
| } |
| } |
| } |
| |
| func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) { |
| if *linksWalked > 255 { |
| return "", false, errTooManyLinks |
| } |
| |
| path = filepath.Join("/", path) |
| if path == "/" { |
| return path, false, nil |
| } |
| realPath := filepath.Join(root, path) |
| |
| fi, err := os.Lstat(realPath) |
| if err != nil { |
| // If path does not yet exist, treat as non-symlink |
| if os.IsNotExist(err) { |
| return path, false, nil |
| } |
| return "", false, err |
| } |
| if fi.Mode()&os.ModeSymlink == 0 { |
| return path, false, nil |
| } |
| newpath, err = os.Readlink(realPath) |
| if err != nil { |
| return "", false, err |
| } |
| *linksWalked++ |
| return newpath, true, nil |
| } |
| |
| func walkLinks(root, path string, linksWalked *int) (string, error) { |
| switch dir, file := filepath.Split(path); { |
| case dir == "": |
| newpath, _, err := walkLink(root, file, linksWalked) |
| return newpath, err |
| case file == "": |
| if os.IsPathSeparator(dir[len(dir)-1]) { |
| if dir == "/" { |
| return dir, nil |
| } |
| return walkLinks(root, dir[:len(dir)-1], linksWalked) |
| } |
| newpath, _, err := walkLink(root, dir, linksWalked) |
| return newpath, err |
| default: |
| newdir, err := walkLinks(root, dir, linksWalked) |
| if err != nil { |
| return "", err |
| } |
| newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked) |
| if err != nil { |
| return "", err |
| } |
| if !islink { |
| return newpath, nil |
| } |
| if filepath.IsAbs(newpath) { |
| return newpath, nil |
| } |
| return filepath.Join(newdir, newpath), nil |
| } |
| } |