1
0
mirror of https://github.com/golang/go synced 2024-11-25 11:37:57 -07:00

os: Getwd: fallback to slow method on ENAMETOOLONG

As of CL 257637, all currently supported platforms have syscall.Getwd
implemented, so the code which deduces wd by traversing up to root
directory is never used and thus can be removed.

Or, as it was suggested by Ian Lance Taylor in CL 607436 review
comments, it can be reused when syscall.Getwd returns ENAMETOOLONG
(which usually happens than the current working dir is longer than
syscall.PathMax).

Let's do that. The only caveat is, such a long path returned from Getwd
couldn't be used for any file-related operations (they will probably
fail with ENAMETOOLONG).

While at it:
 - make the stat(".") code conditional, slightly improving the
   performance on Unix when $PWD is not set;
 - reuse variables dir and err;
 - use openDirNolog instead of openFileNolog to obtain a dirfd;
 - ensure the errors returned are wrapped;
 - document the new functionality;
 - add test cases (which fail before this change).

Change-Id: I60f7a70e6ebb1751699416f587688a1a97305fd7
Reviewed-on: https://go-review.googlesource.com/c/go/+/608635
Reviewed-by: Ian Lance Taylor <iant@google.com>
Auto-Submit: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Kir Kolyshkin 2024-08-26 23:04:40 -07:00 committed by Gopher Robot
parent 0fe6347732
commit bc7e378b34
4 changed files with 119 additions and 15 deletions

View File

@ -9,3 +9,5 @@ package os
import "syscall"
type syscallErrorType = syscall.Errno
const errENOSYS = syscall.ENOSYS

View File

@ -7,3 +7,5 @@ package os
import "syscall"
type syscallErrorType = syscall.ErrorString
var errENOSYS = syscall.NewError("function not implemented")

View File

@ -25,40 +25,52 @@ var getwdCache struct {
// current directory, it is returned.
func Getwd() (dir string, err error) {
if runtime.GOOS == "windows" || runtime.GOOS == "plan9" {
// Use syscall.Getwd directly for
// - plan9: see reasons in CL 89575;
// - windows: syscall implementation is sufficient,
// and we should not rely on $PWD.
dir, err = syscall.Getwd()
return dir, NewSyscallError("getwd", err)
}
// Clumsy but widespread kludge:
// if $PWD is set and matches ".", use it.
dot, err := statNolog(".")
if err != nil {
return "", err
}
var dot FileInfo
dir = Getenv("PWD")
if len(dir) > 0 && dir[0] == '/' {
dot, err = statNolog(".")
if err != nil {
return "", err
}
d, err := statNolog(dir)
if err == nil && SameFile(dot, d) {
return dir, nil
}
// If err is ENAMETOOLONG here, the syscall.Getwd below will
// fail with the same error, too, but let's give it a try
// anyway as the fallback code is much slower.
}
// If the operating system provides a Getwd call, use it.
// Otherwise, we're trying to find our way back to ".".
if syscall.ImplementsGetwd {
var (
s string
e error
)
for {
s, e = syscall.Getwd()
if e != syscall.EINTR {
dir, err = syscall.Getwd()
if err != syscall.EINTR {
break
}
}
return s, NewSyscallError("getwd", e)
if err != syscall.ENAMETOOLONG {
return dir, NewSyscallError("getwd", err)
}
}
// We're trying to find our way back to ".".
if dot == nil {
dot, err = statNolog(".")
if err != nil {
return "", err
}
}
// Apply same kludge but to cached dir instead of $PWD.
getwdCache.Lock()
dir = getwdCache.dir
@ -87,9 +99,9 @@ func Getwd() (dir string, err error) {
dir = ""
for parent := ".."; ; parent = "../" + parent {
if len(parent) >= 1024 { // Sanity check
return "", syscall.ENAMETOOLONG
return "", NewSyscallError("getwd", syscall.ENAMETOOLONG)
}
fd, err := openFileNolog(parent, O_RDONLY, 0)
fd, err := openDirNolog(parent)
if err != nil {
return "", err
}
@ -98,7 +110,14 @@ func Getwd() (dir string, err error) {
names, err := fd.Readdirnames(100)
if err != nil {
fd.Close()
return "", err
// Readdirnames can return io.EOF or other error.
// In any case, we're here because syscall.Getwd
// is not implemented or failed with ENAMETOOLONG,
// so return the most sensible error.
if syscall.ImplementsGetwd {
return "", NewSyscallError("getwd", syscall.ENAMETOOLONG)
}
return "", NewSyscallError("getwd", errENOSYS)
}
for _, name := range names {
d, _ := lstatNolog(parent + "/" + name)

81
src/os/getwd_unix_test.go Normal file
View File

@ -0,0 +1,81 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix
package os_test
import (
"errors"
. "os"
"strings"
"syscall"
"testing"
)
func TestGetwdDeep(t *testing.T) {
testGetwdDeep(t, false)
}
func TestGetwdDeepWithPWDSet(t *testing.T) {
testGetwdDeep(t, true)
}
// testGetwdDeep checks that os.Getwd is able to return paths
// longer than syscall.PathMax (with or without PWD set).
func testGetwdDeep(t *testing.T, setPWD bool) {
dir := t.TempDir()
t.Chdir(dir)
if setPWD {
t.Setenv("PWD", dir)
} else {
// When testing os.Getwd, setting PWD to empty string
// is the same as unsetting it, but the latter would
// be more complicated since we don't have t.Unsetenv.
t.Setenv("PWD", "")
}
name := strings.Repeat("a", 200)
for {
if err := Mkdir(name, 0o700); err != nil {
t.Fatal(err)
}
if err := Chdir(name); err != nil {
t.Fatal(err)
}
if setPWD {
dir += "/" + name
if err := Setenv("PWD", dir); err != nil {
t.Fatal(err)
}
t.Logf(" $PWD len: %d", len(dir))
}
wd, err := Getwd()
t.Logf("Getwd len: %d", len(wd))
if err != nil {
t.Fatal(err)
}
if setPWD && wd != dir {
t.Fatalf("Getwd: want same value as $PWD: %q, got %q", dir, wd)
}
// Ideally the success criterion should be len(wd) > syscall.PathMax,
// but the latter is not public for some platforms, so use Stat(wd).
// When it fails with ENAMETOOLONG, it means:
// - wd is longer than PathMax;
// - Getwd have used the slow fallback code.
//
// To avoid an endless loop here in case Stat keeps working,
// check if len(wd) is above the largest known PathMax among
// all Unix platforms (4096, on Linux).
if _, err := Stat(wd); err != nil || len(wd) > 4096 {
t.Logf("Done; len(wd)=%d", len(wd))
if err != nil && !errors.Is(err, syscall.ENAMETOOLONG) {
t.Fatalf("unexpected Stat error: %v", err)
}
break
}
}
}