Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ospath: add path canonicalization utilities #5309

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ jobs:
- run: go install -mod vendor ./cmd/tilt
# Only run watch tests, because these are currently the only tests that are OS-specific.
# In other Tilt tests, we mock out OS-specific components.
- run: gotestsum --format standard-quiet --junitfile test-results/unit-tests.xml -- -mod vendor ./internal/watch/...
- run: gotestsum --format standard-quiet --junitfile test-results/unit-tests.xml -- -mod vendor ./internal/watch/... ./internal/ospath/...
- store_test_results:
path: test-results
- slack/notify-on-failure:
Expand Down
55 changes: 55 additions & 0 deletions internal/ospath/realpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ospath

// #include <limits.h>
// #include <stdio.h>
// #include <stdlib.h>
import "C"
import (
"path/filepath"
"unsafe"
)

// Evaluate a path to the proper casing, resolving all symlinks.
func Canonicalize(path string) (string, error) {
current := path
remainder := ""
for {
result, err := Realpath(current)
if err == nil {
return filepath.Join(result, remainder), nil
}

next := filepath.Dir(current)
if next == current {
return "", err
}

remainder = filepath.Join(filepath.Base(current), remainder)
current = next
}
}

// Go binding for realpath from glibc.
func Realpath(path string) (string, error) {
// Allocate a C *char for the input.
cstr := C.CString(path)
defer C.free(unsafe.Pointer(cstr))

// Allocate a C *char for the output.
var outputBytes [C.PATH_MAX]byte
outputCstr := (*C.char)(unsafe.Pointer(&outputBytes[0]))
resultCstr, err := C.realpath(cstr, outputCstr)

// CGo error handling doesn't work like normal Go error handling.
// The CGo function returns errno wrapped as an error, but
// may return an error even if the function succeeds.
//
// See:
// https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
// https://bugzilla.redhat.com/show_bug.cgi?id=1916968
result := C.GoString(resultCstr)
if result == "" && err != nil {
return "", err
}
return result, nil
}
47 changes: 47 additions & 0 deletions internal/ospath/realpath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ospath

import (
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Assume that macOS and Windows are case-insensitive, and other operating
// systems are not. This isn't strictly accurate, but is good enough for
// testing.
var isCaseInsensitive = runtime.GOOS == "darwin" || runtime.GOOS == "windows"

func TestCanonicalizeCaseInsensitive(t *testing.T) {
f := NewOspathFixture(t)
defer f.TearDown()

fileA := filepath.Join("Parent", "FileA")
f.TouchFiles([]string{fileA})

if isCaseInsensitive {
result, err := Canonicalize(f.JoinPath("parent", "filea"))
require.NoError(t, err)
assert.Equal(t, f.JoinPath("Parent", "FileA"), result)
} else {
result, err := Canonicalize(f.JoinPath("parent", "filea"))
require.NoError(t, err)
assert.Equal(t, f.JoinPath("parent", "filea"), result)
}
}

func TestCanonicalizeCaseInsensitiveDoesNotExist(t *testing.T) {
f := NewOspathFixture(t)
defer f.TearDown()

_, err := Realpath(f.JoinPath("parent", "filea"))
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "no such file or directory")
}

result, err := Canonicalize(f.JoinPath("parent", "filea"))
require.NoError(t, err)
assert.Equal(t, f.JoinPath("parent", "filea"), result)
}