diff --git a/paths.go b/paths.go index 6a9578f..c242a91 100644 --- a/paths.go +++ b/paths.go @@ -524,15 +524,22 @@ func (p *Path) EquivalentTo(other *Path) bool { if p.Clean().path == other.Clean().path { return true } - absP, err := p.Abs() - if err != nil { - return false + + if infoP, err := p.Stat(); err != nil { + // go ahead with the next test... + } else if infoOther, err := other.Stat(); err != nil { + // go ahead with the next test... + } else if os.SameFile(infoP, infoOther) { + return true } - absOther, err := other.Abs() - if err != nil { + + if absP, err := p.Abs(); err != nil { + return false + } else if absOther, err := other.Abs(); err != nil { return false + } else { + return absP.path == absOther.path } - return absP.path == absOther.path } // Parents returns all the parents directories of the current path. If the path is absolute @@ -557,3 +564,18 @@ func (p *Path) Parents() []*Path { func (p *Path) String() string { return p.path } + +// Canonical return a "canonical" Path for the given filename. +// The meaning of "canonical" is OS-dependent but the goal of this method +// is to always return the same path for a given file (factoring out all the +// possibile ambiguities including, for example, relative paths traversal, +// symlinks, drive volume letter case, etc). +func (p *Path) Canonical() *Path { + canonical := p.Clone() + // https://github.com/golang/go/issues/17084#issuecomment-246645354 + canonical.FollowSymLink() + if absPath, err := canonical.Abs(); err == nil { + canonical = absPath + } + return canonical +} diff --git a/paths_test.go b/paths_test.go index 38ee8cd..8548113 100644 --- a/paths_test.go +++ b/paths_test.go @@ -31,6 +31,7 @@ package paths import ( "path/filepath" + "runtime" "strings" "testing" @@ -311,6 +312,32 @@ func TestEquivalentPaths(t *testing.T) { require.True(t, New("file1", "abc").EquivalentTo(New("file1", "abc", "def", ".."))) require.True(t, wd.Join("file1").EquivalentTo(New("file1"))) require.True(t, wd.Join("file1").EquivalentTo(New("file1", "abc", ".."))) + + if runtime.GOOS == "windows" { + q := New("_testdata", "anotherFile") + r := New("_testdata", "ANOTHE~1") + require.True(t, q.EquivalentTo(r)) + require.True(t, r.EquivalentTo(q)) + } +} + +func TestCanonicalize(t *testing.T) { + wd, err := Getwd() + require.NoError(t, err) + + p := New("_testdata", "anotherFile").Canonical() + require.Equal(t, wd.Join("_testdata", "anotherFile").String(), p.String()) + + p = New("_testdata", "nonexistentFile").Canonical() + require.Equal(t, wd.Join("_testdata", "nonexistentFile").String(), p.String()) + + if runtime.GOOS == "windows" { + q := New("_testdata", "ANOTHE~1").Canonical() + require.Equal(t, wd.Join("_testdata", "anotherFile").String(), q.String()) + + r := New("c:\\").Canonical() + require.Equal(t, "C:\\", r.String()) + } } func TestRelativeTo(t *testing.T) { @@ -341,6 +368,10 @@ func TestRelativeTo(t *testing.T) { func TestWriteToTempFile(t *testing.T) { tmpDir := New("_testdata", "tmp") + err := tmpDir.MkdirAll() + require.NoError(t, err) + defer tmpDir.RemoveAll() + tmpData := []byte("test") tmp, err := WriteToTempFile(tmpData, tmpDir, "prefix") defer tmp.Remove()