Merge pull request #577 from drakkan/lister-close

add CloserListerAt
diff --git a/attrs_unix.go b/attrs_unix.go
index 371ae9b..96ffc03 100644
--- a/attrs_unix.go
+++ b/attrs_unix.go
@@ -1,5 +1,5 @@
-//go:build darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || aix || js
-// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js
+//go:build darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || aix || js || zos
+// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js zos
 
 package sftp
 
diff --git a/client.go b/client.go
index 5f45722..3a9a8bf 100644
--- a/client.go
+++ b/client.go
@@ -2113,13 +2113,12 @@
 // Unsupported flags are ignored.
 func toPflags(f int) uint32 {
 	var out uint32
-	switch f & os.O_WRONLY {
-	case os.O_WRONLY:
-		out |= sshFxfWrite
+	switch f & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
 	case os.O_RDONLY:
 		out |= sshFxfRead
-	}
-	if f&os.O_RDWR == os.O_RDWR {
+	case os.O_WRONLY:
+		out |= sshFxfWrite
+	case os.O_RDWR:
 		out |= sshFxfRead | sshFxfWrite
 	}
 	if f&os.O_APPEND == os.O_APPEND {
@@ -2143,7 +2142,7 @@
 // setuid, setgid and sticky in m, because we've historically supported those
 // bits, and we mask off any non-permission bits.
 func toChmodPerm(m os.FileMode) (perm uint32) {
-	const mask = os.ModePerm | s_ISUID | s_ISGID | s_ISVTX
+	const mask = os.ModePerm | os.FileMode(s_ISUID|s_ISGID|s_ISVTX)
 	perm = uint32(m & mask)
 
 	if m&os.ModeSetuid != 0 {
diff --git a/client_integration_test.go b/client_integration_test.go
index 35ccbea..546ba21 100644
--- a/client_integration_test.go
+++ b/client_integration_test.go
@@ -974,7 +974,7 @@
 	f.Close()
 
 	const allPerm = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky |
-		s_ISUID | s_ISGID | s_ISVTX
+		os.FileMode(s_ISUID|s_ISGID|s_ISVTX)
 
 	for _, c := range []struct {
 		goPerm    os.FileMode
diff --git a/errno_plan9.go b/errno_plan9.go
new file mode 100644
index 0000000..cf9d390
--- /dev/null
+++ b/errno_plan9.go
@@ -0,0 +1,42 @@
+package sftp
+
+import (
+	"os"
+	"syscall"
+)
+
+var EBADF = syscall.NewError("fd out of range or not open")
+
+func wrapPathError(filepath string, err error) error {
+	if errno, ok := err.(syscall.ErrorString); ok {
+		return &os.PathError{Path: filepath, Err: errno}
+	}
+	return err
+}
+
+// translateErrno translates a syscall error number to a SFTP error code.
+func translateErrno(errno syscall.ErrorString) uint32 {
+	switch errno {
+	case "":
+		return sshFxOk
+	case syscall.ENOENT:
+		return sshFxNoSuchFile
+	case syscall.EPERM:
+		return sshFxPermissionDenied
+	}
+
+	return sshFxFailure
+}
+
+func translateSyscallError(err error) (uint32, bool) {
+	switch e := err.(type) {
+	case syscall.ErrorString:
+		return translateErrno(e), true
+	case *os.PathError:
+		debug("statusFromError,pathError: error is %T %#v", e.Err, e.Err)
+		if errno, ok := e.Err.(syscall.ErrorString); ok {
+			return translateErrno(errno), true
+		}
+	}
+	return 0, false
+}
diff --git a/errno_posix.go b/errno_posix.go
new file mode 100644
index 0000000..cd87e1b
--- /dev/null
+++ b/errno_posix.go
@@ -0,0 +1,45 @@
+//go:build !plan9
+// +build !plan9
+
+package sftp
+
+import (
+	"os"
+	"syscall"
+)
+
+const EBADF = syscall.EBADF
+
+func wrapPathError(filepath string, err error) error {
+	if errno, ok := err.(syscall.Errno); ok {
+		return &os.PathError{Path: filepath, Err: errno}
+	}
+	return err
+}
+
+// translateErrno translates a syscall error number to a SFTP error code.
+func translateErrno(errno syscall.Errno) uint32 {
+	switch errno {
+	case 0:
+		return sshFxOk
+	case syscall.ENOENT:
+		return sshFxNoSuchFile
+	case syscall.EACCES, syscall.EPERM:
+		return sshFxPermissionDenied
+	}
+
+	return sshFxFailure
+}
+
+func translateSyscallError(err error) (uint32, bool) {
+	switch e := err.(type) {
+	case syscall.Errno:
+		return translateErrno(e), true
+	case *os.PathError:
+		debug("statusFromError,pathError: error is %T %#v", e.Err, e.Err)
+		if errno, ok := e.Err.(syscall.Errno); ok {
+			return translateErrno(errno), true
+		}
+	}
+	return 0, false
+}
diff --git a/ls_formatting_test.go b/ls_formatting_test.go
index 8d0fefb..d6e5e03 100644
--- a/ls_formatting_test.go
+++ b/ls_formatting_test.go
@@ -3,6 +3,7 @@
 import (
 	"os"
 	"regexp"
+	"runtime"
 	"strings"
 	"testing"
 	"time"
@@ -101,6 +102,12 @@
 	dateTime := strings.Join(fields[5:8], " ")
 	filename := fields[8]
 
+	if runtime.GOOS == "zos" {
+		// User and Group are always only uppercase characters on z/OS
+		user = strings.ToLower(user)
+		group = strings.ToLower(group)
+	}
+
 	// permissions (len 10, "drwxr-xr-x")
 	const (
 		rwxs = "[-r][-w][-xsS]"
diff --git a/ls_unix.go b/ls_unix.go
index 0beba32..aa230dc 100644
--- a/ls_unix.go
+++ b/ls_unix.go
@@ -1,5 +1,5 @@
-//go:build aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || js
-// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js
+//go:build aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || js || zos
+// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js zos
 
 package sftp
 
diff --git a/request-server_test.go b/request-server_test.go
index 64f380c..93011a5 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -780,8 +780,13 @@
 		}
 	}
 	_, err := p.cli.ReadDir("/foo_01")
-	assert.Equal(t, &StatusError{Code: sshFxFailure,
-		msg: " /foo_01: not a directory"}, err)
+	if runtime.GOOS == "zos" {
+		assert.Equal(t, &StatusError{Code: sshFxFailure,
+			msg: " /foo_01: EDC5135I Not a directory."}, err)
+	} else {
+		assert.Equal(t, &StatusError{Code: sshFxFailure,
+			msg: " /foo_01: not a directory"}, err)
+	}
 	_, err = p.cli.ReadDir("/does_not_exist")
 	assert.Equal(t, os.ErrNotExist, err)
 	di, err := p.cli.ReadDir("/")
diff --git a/server_test.go b/server_test.go
index 4cec312..5bac81e 100644
--- a/server_test.go
+++ b/server_test.go
@@ -99,9 +99,14 @@
 // test that server handles concurrent requests correctly
 func TestConcurrentRequests(t *testing.T) {
 	skipIfWindows(t)
-	filename := "/etc/passwd"
-	if runtime.GOOS == "plan9" {
+	var filename string
+	switch runtime.GOOS {
+	case "plan9":
 		filename = "/lib/ndb/local"
+	case "zos":
+		filename = "/etc/.shrc"
+	default:
+		filename = "/etc/passwd"
 	}
 	client, server := clientServerPair(t)
 	defer client.Close()
diff --git a/stat.go b/stat.go
new file mode 100644
index 0000000..2bb2c13
--- /dev/null
+++ b/stat.go
@@ -0,0 +1,94 @@
+package sftp
+
+import (
+	"os"
+
+	sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+// isRegular returns true if the mode describes a regular file.
+func isRegular(mode uint32) bool {
+	return sshfx.FileMode(mode)&sshfx.ModeType == sshfx.ModeRegular
+}
+
+// toFileMode converts sftp filemode bits to the os.FileMode specification
+func toFileMode(mode uint32) os.FileMode {
+	var fm = os.FileMode(mode & 0777)
+
+	switch sshfx.FileMode(mode) & sshfx.ModeType {
+	case sshfx.ModeDevice:
+		fm |= os.ModeDevice
+	case sshfx.ModeCharDevice:
+		fm |= os.ModeDevice | os.ModeCharDevice
+	case sshfx.ModeDir:
+		fm |= os.ModeDir
+	case sshfx.ModeNamedPipe:
+		fm |= os.ModeNamedPipe
+	case sshfx.ModeSymlink:
+		fm |= os.ModeSymlink
+	case sshfx.ModeRegular:
+		// nothing to do
+	case sshfx.ModeSocket:
+		fm |= os.ModeSocket
+	}
+
+	if sshfx.FileMode(mode)&sshfx.ModeSetUID != 0 {
+		fm |= os.ModeSetuid
+	}
+	if sshfx.FileMode(mode)&sshfx.ModeSetGID != 0 {
+		fm |= os.ModeSetgid
+	}
+	if sshfx.FileMode(mode)&sshfx.ModeSticky != 0 {
+		fm |= os.ModeSticky
+	}
+
+	return fm
+}
+
+// fromFileMode converts from the os.FileMode specification to sftp filemode bits
+func fromFileMode(mode os.FileMode) uint32 {
+	ret := sshfx.FileMode(mode & os.ModePerm)
+
+	switch mode & os.ModeType {
+	case os.ModeDevice | os.ModeCharDevice:
+		ret |= sshfx.ModeCharDevice
+	case os.ModeDevice:
+		ret |= sshfx.ModeDevice
+	case os.ModeDir:
+		ret |= sshfx.ModeDir
+	case os.ModeNamedPipe:
+		ret |= sshfx.ModeNamedPipe
+	case os.ModeSymlink:
+		ret |= sshfx.ModeSymlink
+	case 0:
+		ret |= sshfx.ModeRegular
+	case os.ModeSocket:
+		ret |= sshfx.ModeSocket
+	}
+
+	if mode&os.ModeSetuid != 0 {
+		ret |= sshfx.ModeSetUID
+	}
+	if mode&os.ModeSetgid != 0 {
+		ret |= sshfx.ModeSetGID
+	}
+	if mode&os.ModeSticky != 0 {
+		ret |= sshfx.ModeSticky
+	}
+
+	return uint32(ret)
+}
+
+const (
+	s_ISUID = uint32(sshfx.ModeSetUID)
+	s_ISGID = uint32(sshfx.ModeSetGID)
+	s_ISVTX = uint32(sshfx.ModeSticky)
+)
+
+// S_IFMT is a legacy export, and was brought in to support GOOS environments whose sysconfig.S_IFMT may be different from the value used internally by SFTP standards.
+// There should be no reason why you need to import it, or use it, but unexporting it could cause code to break in a way that cannot be readily fixed.
+// As such, we continue to export this value as the value used in the SFTP standard.
+//
+// Deprecated: Remove use of this value, and avoid any future use as well.
+// There is no alternative provided, you should never need to access this value.
+const S_IFMT = uint32(sshfx.ModeType)
diff --git a/stat_plan9.go b/stat_plan9.go
deleted file mode 100644
index 761abdf..0000000
--- a/stat_plan9.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package sftp
-
-import (
-	"os"
-	"syscall"
-)
-
-var EBADF = syscall.NewError("fd out of range or not open")
-
-func wrapPathError(filepath string, err error) error {
-	if errno, ok := err.(syscall.ErrorString); ok {
-		return &os.PathError{Path: filepath, Err: errno}
-	}
-	return err
-}
-
-// translateErrno translates a syscall error number to a SFTP error code.
-func translateErrno(errno syscall.ErrorString) uint32 {
-	switch errno {
-	case "":
-		return sshFxOk
-	case syscall.ENOENT:
-		return sshFxNoSuchFile
-	case syscall.EPERM:
-		return sshFxPermissionDenied
-	}
-
-	return sshFxFailure
-}
-
-func translateSyscallError(err error) (uint32, bool) {
-	switch e := err.(type) {
-	case syscall.ErrorString:
-		return translateErrno(e), true
-	case *os.PathError:
-		debug("statusFromError,pathError: error is %T %#v", e.Err, e.Err)
-		if errno, ok := e.Err.(syscall.ErrorString); ok {
-			return translateErrno(errno), true
-		}
-	}
-	return 0, false
-}
-
-// isRegular returns true if the mode describes a regular file.
-func isRegular(mode uint32) bool {
-	return mode&S_IFMT == syscall.S_IFREG
-}
-
-// toFileMode converts sftp filemode bits to the os.FileMode specification
-func toFileMode(mode uint32) os.FileMode {
-	var fm = os.FileMode(mode & 0777)
-
-	switch mode & S_IFMT {
-	case syscall.S_IFBLK:
-		fm |= os.ModeDevice
-	case syscall.S_IFCHR:
-		fm |= os.ModeDevice | os.ModeCharDevice
-	case syscall.S_IFDIR:
-		fm |= os.ModeDir
-	case syscall.S_IFIFO:
-		fm |= os.ModeNamedPipe
-	case syscall.S_IFLNK:
-		fm |= os.ModeSymlink
-	case syscall.S_IFREG:
-		// nothing to do
-	case syscall.S_IFSOCK:
-		fm |= os.ModeSocket
-	}
-
-	return fm
-}
-
-// fromFileMode converts from the os.FileMode specification to sftp filemode bits
-func fromFileMode(mode os.FileMode) uint32 {
-	ret := uint32(mode & os.ModePerm)
-
-	switch mode & os.ModeType {
-	case os.ModeDevice | os.ModeCharDevice:
-		ret |= syscall.S_IFCHR
-	case os.ModeDevice:
-		ret |= syscall.S_IFBLK
-	case os.ModeDir:
-		ret |= syscall.S_IFDIR
-	case os.ModeNamedPipe:
-		ret |= syscall.S_IFIFO
-	case os.ModeSymlink:
-		ret |= syscall.S_IFLNK
-	case 0:
-		ret |= syscall.S_IFREG
-	case os.ModeSocket:
-		ret |= syscall.S_IFSOCK
-	}
-
-	return ret
-}
-
-// Plan 9 doesn't have setuid, setgid or sticky, but a Plan 9 client should
-// be able to send these bits to a POSIX server.
-const (
-	s_ISUID = 04000
-	s_ISGID = 02000
-	s_ISVTX = 01000
-)
diff --git a/stat_posix.go b/stat_posix.go
deleted file mode 100644
index 5b870e2..0000000
--- a/stat_posix.go
+++ /dev/null
@@ -1,124 +0,0 @@
-//go:build !plan9
-// +build !plan9
-
-package sftp
-
-import (
-	"os"
-	"syscall"
-)
-
-const EBADF = syscall.EBADF
-
-func wrapPathError(filepath string, err error) error {
-	if errno, ok := err.(syscall.Errno); ok {
-		return &os.PathError{Path: filepath, Err: errno}
-	}
-	return err
-}
-
-// translateErrno translates a syscall error number to a SFTP error code.
-func translateErrno(errno syscall.Errno) uint32 {
-	switch errno {
-	case 0:
-		return sshFxOk
-	case syscall.ENOENT:
-		return sshFxNoSuchFile
-	case syscall.EACCES, syscall.EPERM:
-		return sshFxPermissionDenied
-	}
-
-	return sshFxFailure
-}
-
-func translateSyscallError(err error) (uint32, bool) {
-	switch e := err.(type) {
-	case syscall.Errno:
-		return translateErrno(e), true
-	case *os.PathError:
-		debug("statusFromError,pathError: error is %T %#v", e.Err, e.Err)
-		if errno, ok := e.Err.(syscall.Errno); ok {
-			return translateErrno(errno), true
-		}
-	}
-	return 0, false
-}
-
-// isRegular returns true if the mode describes a regular file.
-func isRegular(mode uint32) bool {
-	return mode&S_IFMT == syscall.S_IFREG
-}
-
-// toFileMode converts sftp filemode bits to the os.FileMode specification
-func toFileMode(mode uint32) os.FileMode {
-	var fm = os.FileMode(mode & 0777)
-
-	switch mode & S_IFMT {
-	case syscall.S_IFBLK:
-		fm |= os.ModeDevice
-	case syscall.S_IFCHR:
-		fm |= os.ModeDevice | os.ModeCharDevice
-	case syscall.S_IFDIR:
-		fm |= os.ModeDir
-	case syscall.S_IFIFO:
-		fm |= os.ModeNamedPipe
-	case syscall.S_IFLNK:
-		fm |= os.ModeSymlink
-	case syscall.S_IFREG:
-		// nothing to do
-	case syscall.S_IFSOCK:
-		fm |= os.ModeSocket
-	}
-
-	if mode&syscall.S_ISUID != 0 {
-		fm |= os.ModeSetuid
-	}
-	if mode&syscall.S_ISGID != 0 {
-		fm |= os.ModeSetgid
-	}
-	if mode&syscall.S_ISVTX != 0 {
-		fm |= os.ModeSticky
-	}
-
-	return fm
-}
-
-// fromFileMode converts from the os.FileMode specification to sftp filemode bits
-func fromFileMode(mode os.FileMode) uint32 {
-	ret := uint32(mode & os.ModePerm)
-
-	switch mode & os.ModeType {
-	case os.ModeDevice | os.ModeCharDevice:
-		ret |= syscall.S_IFCHR
-	case os.ModeDevice:
-		ret |= syscall.S_IFBLK
-	case os.ModeDir:
-		ret |= syscall.S_IFDIR
-	case os.ModeNamedPipe:
-		ret |= syscall.S_IFIFO
-	case os.ModeSymlink:
-		ret |= syscall.S_IFLNK
-	case 0:
-		ret |= syscall.S_IFREG
-	case os.ModeSocket:
-		ret |= syscall.S_IFSOCK
-	}
-
-	if mode&os.ModeSetuid != 0 {
-		ret |= syscall.S_ISUID
-	}
-	if mode&os.ModeSetgid != 0 {
-		ret |= syscall.S_ISGID
-	}
-	if mode&os.ModeSticky != 0 {
-		ret |= syscall.S_ISVTX
-	}
-
-	return ret
-}
-
-const (
-	s_ISUID = syscall.S_ISUID
-	s_ISGID = syscall.S_ISGID
-	s_ISVTX = syscall.S_ISVTX
-)
diff --git a/syscall_fixed.go b/syscall_fixed.go
deleted file mode 100644
index e844308..0000000
--- a/syscall_fixed.go
+++ /dev/null
@@ -1,10 +0,0 @@
-//go:build plan9 || windows || (js && wasm)
-// +build plan9 windows js,wasm
-
-// Go defines S_IFMT on windows, plan9 and js/wasm as 0x1f000 instead of
-// 0xf000. None of the the other S_IFxyz values include the "1" (in 0x1f000)
-// which prevents them from matching the bitmask.
-
-package sftp
-
-const S_IFMT = 0xf000
diff --git a/syscall_good.go b/syscall_good.go
deleted file mode 100644
index 5005218..0000000
--- a/syscall_good.go
+++ /dev/null
@@ -1,10 +0,0 @@
-//go:build !plan9 && !windows && (!js || !wasm)
-// +build !plan9
-// +build !windows
-// +build !js !wasm
-
-package sftp
-
-import "syscall"
-
-const S_IFMT = syscall.S_IFMT