add canonical function

It's supposed to work like realpath(3), but implemented in Go. It should
use system-specific syscalls.
This commit is contained in:
Rafal Jeczalik
2015-01-17 14:37:22 +01:00
parent 3f53cab631
commit 76442e1605
5 changed files with 174 additions and 138 deletions
+36 -1
View File
@@ -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 {
+37
View File
@@ -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[:])
}
+101
View File
@@ -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
-33
View File
@@ -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 {
-104
View File
@@ -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