diff --git a/util.go b/util.go index 8c3de02..00abcd4 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,10 @@ package notify -import "os" +import ( + "os" + "path/filepath" + "strings" +) const sep = string(os.PathSeparator) @@ -13,6 +17,37 @@ func nonil(err ...error) error { return nil } +// canonical resolves any symlink in the given path and returns it in a clean form. +// It expects the path to be absolute. It fails to resolve circular symlinks by +// maintaining a simple iteration limit. +// +// TODO(rjeczalik): replace with realpath? +func canonical(p string) (string, error) { + for i, depth := 1, 1; i < len(p); i, depth = i+1, depth+1 { + if depth > 128 { + return "", &os.PathError{Op: "canonical", Path: p, Err: errDepth} + } + if j := strings.IndexRune(p[i:], '/'); j == -1 { + i = len(p) + } else { + i = i + j + } + fi, err := os.Lstat(p[:i]) + if err != nil { + return "", err + } + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + s, err := os.Readlink(p[:i]) + if err != nil { + return "", err + } + p = "/" + s + p[i:] + i = 1 // no guarantee s is canonical, start all over + } + } + return filepath.Clean(p), nil +} + // Joinevents TODO func joinevents(events []Event) (e Event) { if len(events) == 0 { diff --git a/util_darwin_test.go b/util_darwin_test.go new file mode 100644 index 0000000..80983ca --- /dev/null +++ b/util_darwin_test.go @@ -0,0 +1,37 @@ +// +build darwin + +package notify + +import ( + "os" + "testing" +) + +func TestCanonicalDarwin(t *testing.T) { + cases := [...]caseCanonical{ + {"/etc", "/private/etc"}, + {"/etc/defaults", "/private/etc/defaults"}, + {"/etc/hosts", "/private/etc/hosts"}, + {"/tmp", "/private/tmp"}, + {"/var", "/private/var"}, + } + testCanonical(t, cases[:]) +} + +func TestCanonicalDarwinMultiple(t *testing.T) { + etcsym, err := symlink("/etc", "") + if err != nil { + t.Fatal(err) + } + tmpsym, err := symlink("/tmp", "") + if err != nil { + t.Fatal(nonil(err, os.Remove(etcsym))) + } + defer removeall(etcsym, tmpsym) + cases := [...]caseCanonical{ + {etcsym, "/private/etc"}, + {etcsym + "/hosts", "/private/etc/hosts"}, + {tmpsym, "/private/tmp"}, + } + testCanonical(t, cases[:]) +} diff --git a/util_test.go b/util_test.go index a4e474a..ce4ab8e 100644 --- a/util_test.go +++ b/util_test.go @@ -1,10 +1,111 @@ package notify import ( + "io/ioutil" + "os" "path/filepath" "testing" ) +func tmpfile(s string) (string, error) { + f, err := ioutil.TempFile(filepath.Split(s)) + if err != nil { + return "", err + } + if err = nonil(f.Sync(), f.Close()); err != nil { + return "", err + } + return f.Name(), nil +} + +func symlink(src, dst string) (string, error) { + name, err := tmpfile(dst) + if err != nil { + return "", err + } + if err = nonil(os.Remove(name), os.Symlink(src, name)); err != nil { + return "", err + } + return name, nil +} + +func removeall(s ...string) { + for _, s := range s { + os.Remove(s) + } +} + +type caseCanonical struct { + path string + full string +} + +func testCanonical(t *testing.T, cases []caseCanonical) { + for i, cas := range cases { + full, err := canonical(cas.path) + if err != nil { + t.Errorf("want err=nil; got %v (i=%d)", err, i) + continue + } + if full != cas.full { + t.Errorf("want full=%q; got %q (i=%d)", cas.full, full, i) + continue + } + } +} + +func TestCanonical(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatal("os.Getwd()=%v", err) + } + wdsym, err := symlink(wd, "") + if err != nil { + t.Fatalf(`symlink(%q, "")=%v`, wd, err) + } + td := filepath.Join(wd, "testdata") + tdsym, err := symlink(td, td) + if err != nil { + t.Errorf("symlink(%q, %q)=%v", td, td, nonil(err, os.Remove(wdsym))) + } + defer removeall(wdsym, tdsym) + vfstxt := filepath.Join(td, "vfs.txt") + cases := [...]caseCanonical{ + {wdsym, wd}, + {tdsym, td}, + {filepath.Join(wdsym, "notify.go"), filepath.Join(wd, "notify.go")}, + {filepath.Join(tdsym, "vfs.txt"), vfstxt}, + {filepath.Join(wdsym, filepath.Base(tdsym), "vfs.txt"), vfstxt}, + } + testCanonical(t, cases[:]) +} + +func TestCanonicalCircular(t *testing.T) { + tmp1, err := tmpfile("circular") + if err != nil { + t.Fatal(err) + } + tmp2, err := tmpfile("circular") + if err != nil { + t.Fatal(nonil(err, os.Remove(tmp1))) + } + defer removeall(tmp1, tmp2) + // Symlink tmp1 -> tmp2. + if err = nonil(os.Remove(tmp1), os.Symlink(tmp2, tmp1)); err != nil { + t.Fatal(err) + } + // Symlnik tmp2 -> tmp1. + if err = nonil(os.Remove(tmp2), os.Symlink(tmp1, tmp2)); err != nil { + t.Fatal(err) + } + if _, err = canonical(tmp1); err == nil { + t.Fatalf("want canonical(%q)!=nil", tmp1) + } + if _, ok := err.(*os.PathError); !ok { + t.Fatalf("want canonical(%q)=os.PathError; got %T", tmp1, err) + } +} + func TestJoinevents(t *testing.T) { cases := [...]struct { evs []Event diff --git a/watcher_fsevents.go b/watcher_fsevents.go index 585c569..76a9f24 100644 --- a/watcher_fsevents.go +++ b/watcher_fsevents.go @@ -4,8 +4,6 @@ package notify import ( "errors" - "os" - "path/filepath" "strings" "sync/atomic" ) @@ -30,37 +28,6 @@ type FSEvent struct { Flags uint32 } -// canonical resolves any symlink in the given path and returns it in a clean form. -// It expects the path to be absolute. It fails to resolve circular symlinks by -// maintaining a simple iteration limit. -// -// TODO(rjeczalik): replace with realpath? -func canonical(p string) (string, error) { - for i, depth := 1, 1; i < len(p); i, depth = i+1, depth+1 { - if depth > 128 { - return "", &os.PathError{Op: "canonical", Path: p, Err: errDepth} - } - if j := strings.IndexRune(p[i:], '/'); j == -1 { - i = len(p) - } else { - i = i + j - } - fi, err := os.Lstat(p[:i]) - if err != nil { - return "", err - } - if fi.Mode()&os.ModeSymlink == os.ModeSymlink { - s, err := os.Readlink(p[:i]) - if err != nil { - return "", err - } - p = "/" + s + p[i:] - i = 1 // no guarantee s is canonical, start all over - } - } - return filepath.Clean(p), nil -} - // splitflags separates event flags from single set into slice of flags. func splitflags(set uint32) (e []uint32) { for i := uint32(1); set != 0; i, set = i<<1, set>>1 { diff --git a/watcher_fsevents_test.go b/watcher_fsevents_test.go index 6c7b993..5aa533b 100644 --- a/watcher_fsevents_test.go +++ b/watcher_fsevents_test.go @@ -3,114 +3,10 @@ package notify import ( - "io/ioutil" - "os" "reflect" "testing" ) -func tmpfile(s string) (string, error) { - f, err := ioutil.TempFile("/tmp", s) - if err != nil { - return "", err - } - if err = nonil(f.Sync(), f.Close()); err != nil { - return "", err - } - return f.Name(), nil -} - -func symlink(s string) (string, error) { - name, err := tmpfile("symlink") - if err != nil { - return "", err - } - if err = nonil(os.Remove(name), os.Symlink(s, name)); err != nil { - return "", err - } - return name, nil -} - -func removeall(s ...string) { - for _, s := range s { - os.Remove(s) - } -} - -type caseCanonical struct { - path string - full string -} - -func testCanonical(t *testing.T, cases []caseCanonical) { - for i, cas := range cases { - full, err := canonical(cas.path) - if err != nil { - t.Errorf("want err=nil; got %v (i=%d)", err, i) - continue - } - if full != cas.full { - t.Errorf("want full=%q; got %q (i=%d)", cas.full, full, i) - continue - } - } -} - -func TestCanonicalize(t *testing.T) { - cases := [...]caseCanonical{ - {"/etc", "/private/etc"}, - {"/etc/defaults", "/private/etc/defaults"}, - {"/etc/hosts", "/private/etc/hosts"}, - {"/tmp", "/private/tmp"}, - {"/var", "/private/var"}, - } - testCanonical(t, cases[:]) -} - -func TestCanonicalizeMultiple(t *testing.T) { - link1, err := symlink("/etc") - if err != nil { - t.Fatal(err) - } - link2, err := symlink("/tmp") - if err != nil { - t.Fatal(nonil(err, os.Remove(link1))) - } - defer removeall(link1, link2) - cases := [...]caseCanonical{ - {link1, "/private/etc"}, - {link1 + "/hosts", "/private/etc/hosts"}, - {link2, "/private/tmp"}, - } - testCanonical(t, cases[:]) -} - -func TestCanonicalizeCircular(t *testing.T) { - tmp1, err := tmpfile("circular") - if err != nil { - t.Fatal(err) - } - tmp2, err := tmpfile("circular") - if err != nil { - t.Fatal(nonil(err, os.Remove(tmp1))) - } - defer removeall(tmp1, tmp2) - // Symlink tmp1 -> tmp2. - if err = nonil(os.Remove(tmp1), os.Symlink(tmp2, tmp1)); err != nil { - t.Fatal(err) - } - // Symlnik tmp2 -> tmp1. - if err = nonil(os.Remove(tmp2), os.Symlink(tmp1, tmp2)); err != nil { - t.Fatal(err) - } - if _, err = canonical(tmp1); err == nil { - t.Fatalf("want canonical(%q)!=nil", tmp1) - } - if _, ok := err.(*os.PathError); !ok { - t.Fatalf("want canonical(%q)=os.PathError; got %T", tmp1, err) - } -} - func TestSplitflags(t *testing.T) { cases := [...]struct { set uint32