Merge pull request #459 from drakkan/go117

CI: add Go 1.17 and remove 1.15
diff --git a/attrs.go b/attrs.go
index 7020d3a..2bb2d57 100644
--- a/attrs.go
+++ b/attrs.go
@@ -21,29 +21,26 @@
 
 // fileInfo is an artificial type designed to satisfy os.FileInfo.
 type fileInfo struct {
-	name  string
-	size  int64
-	mode  os.FileMode
-	mtime time.Time
-	sys   interface{}
+	name string
+	stat *FileStat
 }
 
 // Name returns the base name of the file.
 func (fi *fileInfo) Name() string { return fi.name }
 
 // Size returns the length in bytes for regular files; system-dependent for others.
-func (fi *fileInfo) Size() int64 { return fi.size }
+func (fi *fileInfo) Size() int64 { return int64(fi.stat.Size) }
 
 // Mode returns file mode bits.
-func (fi *fileInfo) Mode() os.FileMode { return fi.mode }
+func (fi *fileInfo) Mode() os.FileMode { return toFileMode(fi.stat.Mode) }
 
 // ModTime returns the last modification time of the file.
-func (fi *fileInfo) ModTime() time.Time { return fi.mtime }
+func (fi *fileInfo) ModTime() time.Time { return time.Unix(int64(fi.stat.Mtime), 0) }
 
 // IsDir returns true if the file is a directory.
 func (fi *fileInfo) IsDir() bool { return fi.Mode().IsDir() }
 
-func (fi *fileInfo) Sys() interface{} { return fi.sys }
+func (fi *fileInfo) Sys() interface{} { return fi.stat }
 
 // FileStat holds the original unmarshalled values from a call to READDIR or
 // *STAT. It is exported for the purposes of accessing the raw values via
@@ -65,25 +62,21 @@
 	ExtData string
 }
 
-func fileInfoFromStat(st *FileStat, name string) os.FileInfo {
-	fs := &fileInfo{
-		name:  name,
-		size:  int64(st.Size),
-		mode:  toFileMode(st.Mode),
-		mtime: time.Unix(int64(st.Mtime), 0),
-		sys:   st,
+func fileInfoFromStat(stat *FileStat, name string) os.FileInfo {
+	return &fileInfo{
+		name: name,
+		stat: stat,
 	}
-	return fs
 }
 
-func fileStatFromInfo(fi os.FileInfo) (uint32, FileStat) {
+func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) {
 	mtime := fi.ModTime().Unix()
 	atime := mtime
 	var flags uint32 = sshFileXferAttrSize |
 		sshFileXferAttrPermissions |
 		sshFileXferAttrACmodTime
 
-	fileStat := FileStat{
+	fileStat := &FileStat{
 		Size:  uint64(fi.Size()),
 		Mode:  fromFileMode(fi.Mode()),
 		Mtime: uint32(mtime),
@@ -91,83 +84,7 @@
 	}
 
 	// os specific file stat decoding
-	fileStatFromInfoOs(fi, &flags, &fileStat)
+	fileStatFromInfoOs(fi, &flags, fileStat)
 
 	return flags, fileStat
 }
-
-func unmarshalAttrs(b []byte) (*FileStat, []byte) {
-	flags, b := unmarshalUint32(b)
-	return getFileStat(flags, b)
-}
-
-func getFileStat(flags uint32, b []byte) (*FileStat, []byte) {
-	var fs FileStat
-	if flags&sshFileXferAttrSize == sshFileXferAttrSize {
-		fs.Size, b, _ = unmarshalUint64Safe(b)
-	}
-	if flags&sshFileXferAttrUIDGID == sshFileXferAttrUIDGID {
-		fs.UID, b, _ = unmarshalUint32Safe(b)
-	}
-	if flags&sshFileXferAttrUIDGID == sshFileXferAttrUIDGID {
-		fs.GID, b, _ = unmarshalUint32Safe(b)
-	}
-	if flags&sshFileXferAttrPermissions == sshFileXferAttrPermissions {
-		fs.Mode, b, _ = unmarshalUint32Safe(b)
-	}
-	if flags&sshFileXferAttrACmodTime == sshFileXferAttrACmodTime {
-		fs.Atime, b, _ = unmarshalUint32Safe(b)
-		fs.Mtime, b, _ = unmarshalUint32Safe(b)
-	}
-	if flags&sshFileXferAttrExtended == sshFileXferAttrExtended {
-		var count uint32
-		count, b, _ = unmarshalUint32Safe(b)
-		ext := make([]StatExtended, count)
-		for i := uint32(0); i < count; i++ {
-			var typ string
-			var data string
-			typ, b, _ = unmarshalStringSafe(b)
-			data, b, _ = unmarshalStringSafe(b)
-			ext[i] = StatExtended{typ, data}
-		}
-		fs.Extended = ext
-	}
-	return &fs, b
-}
-
-func marshalFileInfo(b []byte, fi os.FileInfo) []byte {
-	// attributes variable struct, and also variable per protocol version
-	// spec version 3 attributes:
-	// uint32   flags
-	// uint64   size           present only if flag SSH_FILEXFER_ATTR_SIZE
-	// uint32   uid            present only if flag SSH_FILEXFER_ATTR_UIDGID
-	// uint32   gid            present only if flag SSH_FILEXFER_ATTR_UIDGID
-	// uint32   permissions    present only if flag SSH_FILEXFER_ATTR_PERMISSIONS
-	// uint32   atime          present only if flag SSH_FILEXFER_ACMODTIME
-	// uint32   mtime          present only if flag SSH_FILEXFER_ACMODTIME
-	// uint32   extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED
-	// string   extended_type
-	// string   extended_data
-	// ...      more extended data (extended_type - extended_data pairs),
-	// 	   so that number of pairs equals extended_count
-
-	flags, fileStat := fileStatFromInfo(fi)
-
-	b = marshalUint32(b, flags)
-	if flags&sshFileXferAttrSize != 0 {
-		b = marshalUint64(b, fileStat.Size)
-	}
-	if flags&sshFileXferAttrUIDGID != 0 {
-		b = marshalUint32(b, fileStat.UID)
-		b = marshalUint32(b, fileStat.GID)
-	}
-	if flags&sshFileXferAttrPermissions != 0 {
-		b = marshalUint32(b, fileStat.Mode)
-	}
-	if flags&sshFileXferAttrACmodTime != 0 {
-		b = marshalUint32(b, fileStat.Atime)
-		b = marshalUint32(b, fileStat.Mtime)
-	}
-
-	return b
-}
diff --git a/attrs_stubs.go b/attrs_stubs.go
index ba72e30..c01f336 100644
--- a/attrs_stubs.go
+++ b/attrs_stubs.go
@@ -1,4 +1,4 @@
-// +build !cgo plan9 windows android
+// +build plan9 windows android
 
 package sftp
 
diff --git a/attrs_test.go b/attrs_test.go
index 18d4f5c..a755df6 100644
--- a/attrs_test.go
+++ b/attrs_test.go
@@ -1,45 +1,8 @@
 package sftp
 
 import (
-	"bytes"
 	"os"
-	"reflect"
-	"testing"
-	"time"
 )
 
 // ensure that attrs implemenst os.FileInfo
 var _ os.FileInfo = new(fileInfo)
-
-var unmarshalAttrsTests = []struct {
-	b    []byte
-	want *fileInfo
-	rest []byte
-}{
-	{marshal(nil, struct{ Flags uint32 }{}), &fileInfo{mtime: time.Unix(int64(0), 0)}, nil},
-	{marshal(nil, struct {
-		Flags uint32
-		Size  uint64
-	}{sshFileXferAttrSize, 20}), &fileInfo{size: 20, mtime: time.Unix(int64(0), 0)}, nil},
-	{marshal(nil, struct {
-		Flags       uint32
-		Size        uint64
-		Permissions uint32
-	}{sshFileXferAttrSize | sshFileXferAttrPermissions, 20, 0644}), &fileInfo{size: 20, mode: os.FileMode(0644), mtime: time.Unix(int64(0), 0)}, nil},
-	{marshal(nil, struct {
-		Flags                 uint32
-		Size                  uint64
-		UID, GID, Permissions uint32
-	}{sshFileXferAttrSize | sshFileXferAttrUIDGID | sshFileXferAttrUIDGID | sshFileXferAttrPermissions, 20, 1000, 1000, 0644}), &fileInfo{size: 20, mode: os.FileMode(0644), mtime: time.Unix(int64(0), 0)}, nil},
-}
-
-func TestUnmarshalAttrs(t *testing.T) {
-	for _, tt := range unmarshalAttrsTests {
-		stat, rest := unmarshalAttrs(tt.b)
-		got := fileInfoFromStat(stat, "")
-		tt.want.sys = got.Sys()
-		if !reflect.DeepEqual(got, tt.want) || !bytes.Equal(tt.rest, rest) {
-			t.Errorf("unmarshalAttrs(%#v): want %#v, %#v, got: %#v, %#v", tt.b, tt.want, tt.rest, got, rest)
-		}
-	}
-}
diff --git a/attrs_unix.go b/attrs_unix.go
index 846b208..d1f4452 100644
--- a/attrs_unix.go
+++ b/attrs_unix.go
@@ -1,5 +1,4 @@
-// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix
-// +build cgo
+// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js
 
 package sftp
 
diff --git a/client.go b/client.go
index 784471b..ce62286 100644
--- a/client.go
+++ b/client.go
@@ -1863,13 +1863,6 @@
 	return f.c.setfstat(f.handle, sshFileXferAttrSize, uint64(size))
 }
 
-func min(a, b int) int {
-	if a > b {
-		return b
-	}
-	return a
-}
-
 // normaliseError normalises an error into a more standard form that can be
 // checked against stdlib errors like io.EOF or os.ErrNotExist.
 func normaliseError(err error) error {
@@ -1892,28 +1885,6 @@
 	}
 }
 
-func unmarshalStatus(id uint32, data []byte) error {
-	sid, data := unmarshalUint32(data)
-	if sid != id {
-		return &unexpectedIDErr{id, sid}
-	}
-	code, data := unmarshalUint32(data)
-	msg, data, _ := unmarshalStringSafe(data)
-	lang, _, _ := unmarshalStringSafe(data)
-	return &StatusError{
-		Code: code,
-		msg:  msg,
-		lang: lang,
-	}
-}
-
-func marshalStatus(b []byte, err StatusError) []byte {
-	b = marshalUint32(b, err.Code)
-	b = marshalString(b, err.msg)
-	b = marshalString(b, err.lang)
-	return b
-}
-
 // flags converts the flags passed to OpenFile into ssh flags.
 // Unsupported flags are ignored.
 func flags(f int) uint32 {
diff --git a/client_integration_test.go b/client_integration_test.go
index 5fe5099..933897b 100644
--- a/client_integration_test.go
+++ b/client_integration_test.go
@@ -2313,7 +2313,12 @@
 		}
 
 		for offset < size {
-			n, err := f2.Write(data[offset:min(len(data), offset+bufsize)])
+			buf := data[offset:]
+			if len(buf) > bufsize {
+				buf = buf[:bufsize]
+			}
+
+			n, err := f2.Write(buf)
 			if err != nil {
 				b.Fatal(err)
 			}
diff --git a/client_test.go b/client_test.go
index 8a8c51b..4577ca2 100644
--- a/client_test.go
+++ b/client_test.go
@@ -5,7 +5,6 @@
 	"errors"
 	"io"
 	"os"
-	"reflect"
 	"testing"
 
 	"github.com/kr/fs"
@@ -89,64 +88,6 @@
 	}
 }
 
-func TestUnmarshalStatus(t *testing.T) {
-	requestID := uint32(1)
-
-	id := marshalUint32([]byte{}, requestID)
-	idCode := marshalUint32(id, sshFxFailure)
-	idCodeMsg := marshalString(idCode, "err msg")
-	idCodeMsgLang := marshalString(idCodeMsg, "lang tag")
-
-	var tests = []struct {
-		desc   string
-		reqID  uint32
-		status []byte
-		want   error
-	}{
-		{
-			desc:   "well-formed status",
-			reqID:  1,
-			status: idCodeMsgLang,
-			want: &StatusError{
-				Code: sshFxFailure,
-				msg:  "err msg",
-				lang: "lang tag",
-			},
-		},
-		{
-			desc:   "missing error message and language tag",
-			reqID:  1,
-			status: idCode,
-			want: &StatusError{
-				Code: sshFxFailure,
-			},
-		},
-		{
-			desc:   "missing language tag",
-			reqID:  1,
-			status: idCodeMsg,
-			want: &StatusError{
-				Code: sshFxFailure,
-				msg:  "err msg",
-			},
-		},
-		{
-			desc:   "request identifier mismatch",
-			reqID:  2,
-			status: idCodeMsgLang,
-			want:   &unexpectedIDErr{2, requestID},
-		},
-	}
-
-	for _, tt := range tests {
-		got := unmarshalStatus(tt.reqID, tt.status)
-		if !reflect.DeepEqual(got, tt.want) {
-			t.Errorf("unmarshalStatus(%v, %v), test %q\n- want: %#v\n-  got: %#v",
-				requestID, tt.status, tt.desc, tt.want, got)
-		}
-	}
-}
-
 type packetSizeTest struct {
 	size  int
 	valid bool
diff --git a/examples/buffered-read-benchmark/main.go b/examples/buffered-read-benchmark/main.go
index 36ac6d7..7f2adc4 100644
--- a/examples/buffered-read-benchmark/main.go
+++ b/examples/buffered-read-benchmark/main.go
@@ -40,8 +40,8 @@
 	}
 
 	config := ssh.ClientConfig{
-		User: *USER,
-		Auth: auths,
+		User:            *USER,
+		Auth:            auths,
 		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 	}
 	addr := fmt.Sprintf("%s:%d", *HOST, *PORT)
diff --git a/examples/buffered-write-benchmark/main.go b/examples/buffered-write-benchmark/main.go
index d1babed..7a0594c 100644
--- a/examples/buffered-write-benchmark/main.go
+++ b/examples/buffered-write-benchmark/main.go
@@ -40,8 +40,8 @@
 	}
 
 	config := ssh.ClientConfig{
-		User: *USER,
-		Auth: auths,
+		User:            *USER,
+		Auth:            auths,
 		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 	}
 	addr := fmt.Sprintf("%s:%d", *HOST, *PORT)
diff --git a/examples/streaming-read-benchmark/main.go b/examples/streaming-read-benchmark/main.go
index 87afc5a..3f0f489 100644
--- a/examples/streaming-read-benchmark/main.go
+++ b/examples/streaming-read-benchmark/main.go
@@ -41,8 +41,8 @@
 	}
 
 	config := ssh.ClientConfig{
-		User: *USER,
-		Auth: auths,
+		User:            *USER,
+		Auth:            auths,
 		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 	}
 	addr := fmt.Sprintf("%s:%d", *HOST, *PORT)
diff --git a/examples/streaming-write-benchmark/main.go b/examples/streaming-write-benchmark/main.go
index 8f432d3..2139d97 100644
--- a/examples/streaming-write-benchmark/main.go
+++ b/examples/streaming-write-benchmark/main.go
@@ -41,8 +41,8 @@
 	}
 
 	config := ssh.ClientConfig{
-		User: *USER,
-		Auth: auths,
+		User:            *USER,
+		Auth:            auths,
 		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 	}
 	addr := fmt.Sprintf("%s:%d", *HOST, *PORT)
diff --git a/internal/encoding/ssh/filexfer/response_packets_test.go b/internal/encoding/ssh/filexfer/response_packets_test.go
index 9468665..765f172 100644
--- a/internal/encoding/ssh/filexfer/response_packets_test.go
+++ b/internal/encoding/ssh/filexfer/response_packets_test.go
@@ -174,7 +174,7 @@
 
 	p := &NamePacket{
 		Entries: []*NameEntry{
-			&NameEntry{
+			{
 				Filename: filename + "1",
 				Longname: longname + "1",
 				Attrs: Attributes{
@@ -182,7 +182,7 @@
 					Permissions: perms | 1,
 				},
 			},
-			&NameEntry{
+			{
 				Filename: filename + "2",
 				Longname: longname + "2",
 				Attrs: Attributes{
diff --git a/ls_formatting.go b/ls_formatting.go
new file mode 100644
index 0000000..e083e22
--- /dev/null
+++ b/ls_formatting.go
@@ -0,0 +1,81 @@
+package sftp
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/user"
+	"strconv"
+	"time"
+
+	sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+func lsFormatID(id uint32) string {
+	return strconv.FormatUint(uint64(id), 10)
+}
+
+type osIDLookup struct{}
+
+func (osIDLookup) Filelist(*Request) (ListerAt, error) {
+	return nil, errors.New("unimplemented stub")
+}
+
+func (osIDLookup) LookupUserName(uid string) string {
+	u, err := user.LookupId(uid)
+	if err != nil {
+		return uid
+	}
+
+	return u.Username
+}
+
+func (osIDLookup) LookupGroupName(gid string) string {
+	g, err := user.LookupGroupId(gid)
+	if err != nil {
+		return gid
+	}
+
+	return g.Name
+}
+
+// runLs formats the FileInfo as per `ls -l` style, which is in the 'longname' field of a SSH_FXP_NAME entry.
+// This is a fairly simple implementation, just enough to look close to openssh in simple cases.
+func runLs(idLookup NameLookupFileLister, dirent os.FileInfo) string {
+	// example from openssh sftp server:
+	// crw-rw-rw-    1 root     wheel           0 Jul 31 20:52 ttyvd
+	// format:
+	// {directory / char device / etc}{rwxrwxrwx}  {number of links} owner group size month day [time (this year) | year (otherwise)] name
+
+	symPerms := sshfx.FileMode(fromFileMode(dirent.Mode())).String()
+
+	var numLinks uint64 = 1
+	uid, gid := "0", "0"
+
+	switch sys := dirent.Sys().(type) {
+	case *sshfx.Attributes:
+		uid = lsFormatID(sys.UID)
+		gid = lsFormatID(sys.GID)
+	case *FileStat:
+		uid = lsFormatID(sys.UID)
+		gid = lsFormatID(sys.GID)
+	default:
+		numLinks, uid, gid = lsLinksUIDGID(dirent)
+	}
+
+	if idLookup != nil {
+		uid, gid = idLookup.LookupUserName(uid), idLookup.LookupGroupName(gid)
+	}
+
+	mtime := dirent.ModTime()
+	date := mtime.Format("Jan 2")
+
+	var yearOrTime string
+	if mtime.Before(time.Now().AddDate(0, -6, 0)) {
+		yearOrTime = mtime.Format("2006")
+	} else {
+		yearOrTime = mtime.Format("15:04")
+	}
+
+	return fmt.Sprintf("%s %4d %-8s %-8s %8d %s %5s %s", symPerms, numLinks, uid, gid, dirent.Size(), date, yearOrTime, dirent.Name())
+}
diff --git a/ls_formatting_test.go b/ls_formatting_test.go
new file mode 100644
index 0000000..c9c63a1
--- /dev/null
+++ b/ls_formatting_test.go
@@ -0,0 +1,172 @@
+package sftp
+
+import (
+	"os"
+	"regexp"
+	"strings"
+	"testing"
+	"time"
+)
+
+const (
+	typeDirectory = "d"
+	typeFile      = "[^d]"
+)
+
+func TestRunLsWithExamplesDirectory(t *testing.T) {
+	path := "examples"
+	item, _ := os.Stat(path)
+	result := runLs(nil, item)
+	runLsTestHelper(t, result, typeDirectory, path)
+}
+
+func TestRunLsWithLicensesFile(t *testing.T) {
+	path := "LICENSE"
+	item, _ := os.Stat(path)
+	result := runLs(nil, item)
+	runLsTestHelper(t, result, typeFile, path)
+}
+
+func TestRunLsWithExamplesDirectoryWithOSLookup(t *testing.T) {
+	path := "examples"
+	item, _ := os.Stat(path)
+	result := runLs(osIDLookup{}, item)
+	runLsTestHelper(t, result, typeDirectory, path)
+}
+
+func TestRunLsWithLicensesFileWithOSLookup(t *testing.T) {
+	path := "LICENSE"
+	item, _ := os.Stat(path)
+	result := runLs(osIDLookup{}, item)
+	runLsTestHelper(t, result, typeFile, path)
+}
+
+/*
+   The format of the `longname' field is unspecified by this protocol.
+   It MUST be suitable for use in the output of a directory listing
+   command (in fact, the recommended operation for a directory listing
+   command is to simply display this data).  However, clients SHOULD NOT
+   attempt to parse the longname field for file attributes; they SHOULD
+   use the attrs field instead.
+
+    The recommended format for the longname field is as follows:
+
+        -rwxr-xr-x   1 mjos     staff      348911 Mar 25 14:29 t-filexfer
+        1234567890 123 12345678 12345678 12345678 123456789012
+
+   Here, the first line is sample output, and the second field indicates
+   widths of the various fields.  Fields are separated by spaces.  The
+   first field lists file permissions for user, group, and others; the
+   second field is link count; the third field is the name of the user
+   who owns the file; the fourth field is the name of the group that
+   owns the file; the fifth field is the size of the file in bytes; the
+   sixth field (which actually may contain spaces, but is fixed to 12
+   characters) is the file modification time, and the seventh field is
+   the file name.  Each field is specified to be a minimum of certain
+   number of character positions (indicated by the second line above),
+   but may also be longer if the data does not fit in the specified
+   length.
+
+    The SSH_FXP_ATTRS response has the following format:
+
+        uint32     id
+        ATTRS      attrs
+
+   where `id' is the request identifier, and `attrs' is the returned
+   file attributes as described in Section ``File Attributes''.
+
+   N.B.: FileZilla does parse this ls formatting, and so not rendering it
+   on any particular GOOS/GOARCH can cause compatibility issues with this client.
+*/
+func runLsTestHelper(t *testing.T, result, expectedType, path string) {
+	// using regular expressions to make tests work on all systems
+	// a virtual file system (like afero) would be needed to mock valid filesystem checks
+	// expected layout is:
+	// drwxr-xr-x   8 501      20            272 Aug  9 19:46 examples
+
+	t.Log(result)
+
+	sparce := strings.Split(result, " ")
+
+	var fields []string
+	for _, field := range sparce {
+		if field == "" {
+			continue
+		}
+
+		fields = append(fields, field)
+	}
+
+	perms, linkCnt, user, group, size := fields[0], fields[1], fields[2], fields[3], fields[4]
+	dateTime := strings.Join(fields[5:8], " ")
+	filename := fields[8]
+
+	// permissions (len 10, "drwxr-xr-x")
+	const (
+		rwxs = "[-r][-w][-xsS]"
+		rwxt = "[-r][-w][-xtT]"
+	)
+	if ok, err := regexp.MatchString("^"+expectedType+rwxs+rwxs+rwxt+"$", perms); !ok {
+		if err != nil {
+			t.Fatal("unexpected error:", err)
+		}
+
+		t.Errorf("runLs(%q): permission field mismatch, expected dir, got: %#v, err: %#v", path, perms, err)
+	}
+
+	// link count (len 3, number)
+	const (
+		number = "(?:[0-9]+)"
+	)
+	if ok, err := regexp.MatchString("^"+number+"$", linkCnt); !ok {
+		if err != nil {
+			t.Fatal("unexpected error:", err)
+		}
+
+		t.Errorf("runLs(%q): link count field mismatch, got: %#v, err: %#v", path, linkCnt, err)
+	}
+
+	// username / uid (len 8, number or string)
+	const (
+		name = "(?:[a-z_][a-z0-9_]*)"
+	)
+	if ok, err := regexp.MatchString("^(?:"+number+"|"+name+")+$", user); !ok {
+		if err != nil {
+			t.Fatal("unexpected error:", err)
+		}
+
+		t.Errorf("runLs(%q): username / uid mismatch, expected user, got: %#v, err: %#v", path, user, err)
+	}
+
+	// groupname / gid (len 8, number or string)
+	if ok, err := regexp.MatchString("^(?:"+number+"|"+name+")+$", group); !ok {
+		if err != nil {
+			t.Fatal("unexpected error:", err)
+		}
+
+		t.Errorf("runLs(%q): groupname / gid mismatch, expected group, got: %#v, err: %#v", path, group, err)
+	}
+
+	// filesize (len 8)
+	if ok, err := regexp.MatchString("^"+number+"$", size); !ok {
+		if err != nil {
+			t.Fatal("unexpected error:", err)
+		}
+
+		t.Errorf("runLs(%q): filesize field mismatch, expected size in bytes, got: %#v, err: %#v", path, size, err)
+	}
+
+	// mod time (len 12, e.g. Aug  9 19:46)
+	_, err := time.Parse("Jan 2 15:04", dateTime)
+	if err != nil {
+		_, err = time.Parse("Jan 2 2006", dateTime)
+		if err != nil {
+			t.Errorf("runLs.dateTime = %#v should match `Jan 2 15:04` or `Jan 2 2006`: %+v", dateTime, err)
+		}
+	}
+
+	// filename
+	if path != filename {
+		t.Errorf("runLs.filename = %#v, expected: %#v", filename, path)
+	}
+}
diff --git a/ls_plan9.go b/ls_plan9.go
new file mode 100644
index 0000000..a16a3ea
--- /dev/null
+++ b/ls_plan9.go
@@ -0,0 +1,21 @@
+// +build plan9
+
+package sftp
+
+import (
+	"os"
+	"syscall"
+)
+
+func lsLinksUIDGID(fi os.FileInfo) (numLinks uint64, uid, gid string) {
+	numLinks = 1
+	uid, gid = "0", "0"
+
+	switch sys := fi.Sys().(type) {
+	case *syscall.Dir:
+		uid = sys.Uid
+		gid = sys.Gid
+	}
+
+	return numLinks, uid, gid
+}
diff --git a/ls_stub.go b/ls_stub.go
new file mode 100644
index 0000000..6dec393
--- /dev/null
+++ b/ls_stub.go
@@ -0,0 +1,11 @@
+// +build windows android
+
+package sftp
+
+import (
+	"os"
+)
+
+func lsLinksUIDGID(fi os.FileInfo) (numLinks uint64, uid, gid string) {
+	return 1, "0", "0"
+}
diff --git a/ls_unix.go b/ls_unix.go
new file mode 100644
index 0000000..59ccffd
--- /dev/null
+++ b/ls_unix.go
@@ -0,0 +1,23 @@
+// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js
+
+package sftp
+
+import (
+	"os"
+	"syscall"
+)
+
+func lsLinksUIDGID(fi os.FileInfo) (numLinks uint64, uid, gid string) {
+	numLinks = 1
+	uid, gid = "0", "0"
+
+	switch sys := fi.Sys().(type) {
+	case *syscall.Stat_t:
+		numLinks = uint64(sys.Nlink)
+		uid = lsFormatID(sys.Uid)
+		gid = lsFormatID(sys.Gid)
+	default:
+	}
+
+	return numLinks, uid, gid
+}
diff --git a/packet.go b/packet.go
index 2b2e592..50ca069 100644
--- a/packet.go
+++ b/packet.go
@@ -37,6 +37,50 @@
 	return append(marshalUint32(b, uint32(len(v))), v...)
 }
 
+func marshalFileInfo(b []byte, fi os.FileInfo) []byte {
+	// attributes variable struct, and also variable per protocol version
+	// spec version 3 attributes:
+	// uint32   flags
+	// uint64   size           present only if flag SSH_FILEXFER_ATTR_SIZE
+	// uint32   uid            present only if flag SSH_FILEXFER_ATTR_UIDGID
+	// uint32   gid            present only if flag SSH_FILEXFER_ATTR_UIDGID
+	// uint32   permissions    present only if flag SSH_FILEXFER_ATTR_PERMISSIONS
+	// uint32   atime          present only if flag SSH_FILEXFER_ACMODTIME
+	// uint32   mtime          present only if flag SSH_FILEXFER_ACMODTIME
+	// uint32   extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED
+	// string   extended_type
+	// string   extended_data
+	// ...      more extended data (extended_type - extended_data pairs),
+	// 	   so that number of pairs equals extended_count
+
+	flags, fileStat := fileStatFromInfo(fi)
+
+	b = marshalUint32(b, flags)
+	if flags&sshFileXferAttrSize != 0 {
+		b = marshalUint64(b, fileStat.Size)
+	}
+	if flags&sshFileXferAttrUIDGID != 0 {
+		b = marshalUint32(b, fileStat.UID)
+		b = marshalUint32(b, fileStat.GID)
+	}
+	if flags&sshFileXferAttrPermissions != 0 {
+		b = marshalUint32(b, fileStat.Mode)
+	}
+	if flags&sshFileXferAttrACmodTime != 0 {
+		b = marshalUint32(b, fileStat.Atime)
+		b = marshalUint32(b, fileStat.Mtime)
+	}
+
+	return b
+}
+
+func marshalStatus(b []byte, err StatusError) []byte {
+	b = marshalUint32(b, err.Code)
+	b = marshalString(b, err.msg)
+	b = marshalString(b, err.lang)
+	return b
+}
+
 func marshal(b []byte, v interface{}) []byte {
 	if v == nil {
 		return b
@@ -115,6 +159,63 @@
 	return string(b[:n]), b[n:], nil
 }
 
+func unmarshalAttrs(b []byte) (*FileStat, []byte) {
+	flags, b := unmarshalUint32(b)
+	return unmarshalFileStat(flags, b)
+}
+
+func unmarshalFileStat(flags uint32, b []byte) (*FileStat, []byte) {
+	var fs FileStat
+	if flags&sshFileXferAttrSize == sshFileXferAttrSize {
+		fs.Size, b, _ = unmarshalUint64Safe(b)
+	}
+	if flags&sshFileXferAttrUIDGID == sshFileXferAttrUIDGID {
+		fs.UID, b, _ = unmarshalUint32Safe(b)
+	}
+	if flags&sshFileXferAttrUIDGID == sshFileXferAttrUIDGID {
+		fs.GID, b, _ = unmarshalUint32Safe(b)
+	}
+	if flags&sshFileXferAttrPermissions == sshFileXferAttrPermissions {
+		fs.Mode, b, _ = unmarshalUint32Safe(b)
+	}
+	if flags&sshFileXferAttrACmodTime == sshFileXferAttrACmodTime {
+		fs.Atime, b, _ = unmarshalUint32Safe(b)
+		fs.Mtime, b, _ = unmarshalUint32Safe(b)
+	}
+	if flags&sshFileXferAttrExtended == sshFileXferAttrExtended {
+		var count uint32
+		count, b, _ = unmarshalUint32Safe(b)
+		ext := make([]StatExtended, count)
+		for i := uint32(0); i < count; i++ {
+			var typ string
+			var data string
+			typ, b, _ = unmarshalStringSafe(b)
+			data, b, _ = unmarshalStringSafe(b)
+			ext[i] = StatExtended{
+				ExtType: typ,
+				ExtData: data,
+			}
+		}
+		fs.Extended = ext
+	}
+	return &fs, b
+}
+
+func unmarshalStatus(id uint32, data []byte) error {
+	sid, data := unmarshalUint32(data)
+	if sid != id {
+		return &unexpectedIDErr{id, sid}
+	}
+	code, data := unmarshalUint32(data)
+	msg, data, _ := unmarshalStringSafe(data)
+	lang, _, _ := unmarshalStringSafe(data)
+	return &StatusError{
+		Code: code,
+		msg:  msg,
+		lang: lang,
+	}
+}
+
 type packetMarshaler interface {
 	marshalPacket() (header, payload []byte, err error)
 }
@@ -638,12 +739,17 @@
 const dataHeaderLen = 4 + 1 + 4 + 4
 
 func (p *sshFxpReadPacket) getDataSlice(alloc *allocator, orderID uint32) []byte {
-	dataLen := clamp(p.Len, maxTxPacket)
+	dataLen := p.Len
+	if dataLen > maxTxPacket {
+		dataLen = maxTxPacket
+	}
+
 	if alloc != nil {
 		// GetPage returns a slice with capacity = maxMsgLength this is enough to avoid new allocations in
 		// sshFxpDataPacket.MarshalBinary
 		return alloc.GetPage(orderID)[:dataLen]
 	}
+
 	// allocate with extra space for the header
 	return make([]byte, dataLen, dataLen+dataHeaderLen)
 }
@@ -1016,6 +1122,7 @@
 	return header, buf.Bytes(), err
 }
 
+// MarshalBinary encodes the StatVFS as an SSH_FXP_EXTENDED_REPLY packet.
 func (p *StatVFS) MarshalBinary() ([]byte, error) {
 	header, payload, err := p.marshalPacket()
 	return append(header, payload...), err
diff --git a/packet_test.go b/packet_test.go
index c7deb5a..cbee5e4 100644
--- a/packet_test.go
+++ b/packet_test.go
@@ -6,6 +6,7 @@
 	"errors"
 	"io/ioutil"
 	"os"
+	"reflect"
 	"testing"
 )
 
@@ -217,6 +218,138 @@
 	}
 }
 
+func TestUnmarshalAttrs(t *testing.T) {
+	var tests = []struct {
+		b    []byte
+		want *FileStat
+	}{
+		{
+			b:    []byte{0x00, 0x00, 0x00, 0x00},
+			want: &FileStat{},
+		},
+		{
+			b: []byte{
+				0x00, 0x00, 0x00, byte(sshFileXferAttrSize),
+				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20,
+			},
+			want: &FileStat{
+				Size: 20,
+			},
+		},
+		{
+			b: []byte{
+				0x00, 0x00, 0x00, byte(sshFileXferAttrSize | sshFileXferAttrPermissions),
+				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20,
+				0x00, 0x00, 0x01, 0xA4,
+			},
+			want: &FileStat{
+				Size: 20,
+				Mode: 0644,
+			},
+		},
+		{
+			b: []byte{
+				0x00, 0x00, 0x00, byte(sshFileXferAttrSize | sshFileXferAttrPermissions | sshFileXferAttrUIDGID),
+				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20,
+				0x00, 0x00, 0x03, 0xE8,
+				0x00, 0x00, 0x03, 0xE9,
+				0x00, 0x00, 0x01, 0xA4,
+			},
+			want: &FileStat{
+				Size: 20,
+				Mode: 0644,
+				UID:  1000,
+				GID:  1001,
+			},
+		},
+		{
+			b: []byte{
+				0x00, 0x00, 0x00, byte(sshFileXferAttrSize | sshFileXferAttrPermissions | sshFileXferAttrUIDGID | sshFileXferAttrACmodTime),
+				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20,
+				0x00, 0x00, 0x03, 0xE8,
+				0x00, 0x00, 0x03, 0xE9,
+				0x00, 0x00, 0x01, 0xA4,
+				0x00, 0x00, 0x00, 42,
+				0x00, 0x00, 0x00, 13,
+			},
+			want: &FileStat{
+				Size:  20,
+				Mode:  0644,
+				UID:   1000,
+				GID:   1001,
+				Atime: 42,
+				Mtime: 13,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		got, _ := unmarshalAttrs(tt.b)
+		if !reflect.DeepEqual(got, tt.want) {
+			t.Errorf("unmarshalAttrs(% X):\n-  got: %#v\n- want: %#v", tt.b, got, tt.want)
+		}
+	}
+}
+
+func TestUnmarshalStatus(t *testing.T) {
+	var requestID uint32 = 1
+
+	id := marshalUint32(nil, requestID)
+	idCode := marshalUint32(id, sshFxFailure)
+	idCodeMsg := marshalString(idCode, "err msg")
+	idCodeMsgLang := marshalString(idCodeMsg, "lang tag")
+
+	var tests = []struct {
+		desc   string
+		reqID  uint32
+		status []byte
+		want   error
+	}{
+		{
+			desc:   "well-formed status",
+			status: idCodeMsgLang,
+			want: &StatusError{
+				Code: sshFxFailure,
+				msg:  "err msg",
+				lang: "lang tag",
+			},
+		},
+		{
+			desc:   "missing language tag",
+			status: idCodeMsg,
+			want: &StatusError{
+				Code: sshFxFailure,
+				msg:  "err msg",
+			},
+		},
+		{
+			desc:   "missing error message and language tag",
+			status: idCode,
+			want: &StatusError{
+				Code: sshFxFailure,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			got := unmarshalStatus(1, tt.status)
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("unmarshalStatus(1, % X):\n-  got: %#v\n- want: %#v", tt.status, got, tt.want)
+			}
+		})
+	}
+
+	got := unmarshalStatus(2, idCodeMsgLang)
+	want := &unexpectedIDErr{
+		want: 2,
+		got:  1,
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("unmarshalStatus(2, % X):\n-  got: %#v\n- want: %#v", idCodeMsgLang, got, want)
+	}
+}
+
 func TestSendPacket(t *testing.T) {
 	var tests = []struct {
 		packet encoding.BinaryMarshaler
diff --git a/request-attrs.go b/request-attrs.go
index 7c2e5c1..b5c95b4 100644
--- a/request-attrs.go
+++ b/request-attrs.go
@@ -58,6 +58,6 @@
 // Attributes parses file attributes byte blob and return them in a
 // FileStat object.
 func (r *Request) Attributes() *FileStat {
-	fs, _ := getFileStat(r.Flags, r.Attrs)
+	fs, _ := unmarshalFileStat(r.Flags, r.Attrs)
 	return fs
 }
diff --git a/request-attrs_test.go b/request-attrs_test.go
index 423a17c..3e1b096 100644
--- a/request-attrs_test.go
+++ b/request-attrs_test.go
@@ -33,7 +33,7 @@
 	at := []byte{}
 	at = marshalUint32(at, 1)
 	at = marshalUint32(at, 2)
-	testFs, _ := getFileStat(fl, at)
+	testFs, _ := unmarshalFileStat(fl, at)
 	assert.Equal(t, fa, *testFs)
 	// Size and Mode
 	fa = FileStat{Mode: 700, Size: 99}
@@ -41,7 +41,7 @@
 	at = []byte{}
 	at = marshalUint64(at, 99)
 	at = marshalUint32(at, 700)
-	testFs, _ = getFileStat(fl, at)
+	testFs, _ = unmarshalFileStat(fl, at)
 	assert.Equal(t, fa, *testFs)
 	// FileMode
 	assert.True(t, testFs.FileMode().IsRegular())
@@ -50,7 +50,7 @@
 }
 
 func TestRequestAttributesEmpty(t *testing.T) {
-	fs, b := getFileStat(sshFileXferAttrAll, nil)
+	fs, b := unmarshalFileStat(sshFileXferAttrAll, nil)
 	assert.Equal(t, &FileStat{
 		Extended: []StatExtended{},
 	}, fs)
diff --git a/request-interfaces.go b/request-interfaces.go
index 41e3327..c8c424c 100644
--- a/request-interfaces.go
+++ b/request-interfaces.go
@@ -95,6 +95,14 @@
 	RealPath(string) string
 }
 
+// NameLookupFileLister is a FileLister that implmeents the LookupUsername and LookupGroupName methods.
+// If this interface is implemented, then longname ls formatting will use these to convert usernames and groupnames.
+type NameLookupFileLister interface {
+	FileLister
+	LookupUserName(string) string
+	LookupGroupName(string) string
+}
+
 // ListerAt does for file lists what io.ReaderAt does for files.
 // ListAt should return the number of entries copied and an io.EOF
 // error if at end of list. This is testable by comparing how many you
diff --git a/request-server.go b/request-server.go
index 62f5aa5..5fa828b 100644
--- a/request-server.go
+++ b/request-server.go
@@ -22,12 +22,14 @@
 
 // RequestServer abstracts the sftp protocol with an http request-like protocol
 type RequestServer struct {
+	Handlers Handlers
+
 	*serverConn
-	Handlers        Handlers
-	pktMgr          *packetManager
-	openRequests    map[string]*Request
-	openRequestLock sync.RWMutex
-	handleCount     int
+	pktMgr *packetManager
+
+	mu           sync.RWMutex
+	handleCount  int
+	openRequests map[string]*Request
 }
 
 // A RequestServerOption is a function which applies configuration to a RequestServer.
@@ -55,9 +57,11 @@
 		},
 	}
 	rs := &RequestServer{
-		serverConn:   svrConn,
-		Handlers:     h,
-		pktMgr:       newPktMgr(svrConn),
+		Handlers: h,
+
+		serverConn: svrConn,
+		pktMgr:     newPktMgr(svrConn),
+
 		openRequests: make(map[string]*Request),
 	}
 
@@ -69,13 +73,15 @@
 
 // New Open packet/Request
 func (rs *RequestServer) nextRequest(r *Request) string {
-	rs.openRequestLock.Lock()
-	defer rs.openRequestLock.Unlock()
+	rs.mu.Lock()
+	defer rs.mu.Unlock()
+
 	rs.handleCount++
-	handle := strconv.Itoa(rs.handleCount)
-	r.handle = handle
-	rs.openRequests[handle] = r
-	return handle
+
+	r.handle = strconv.Itoa(rs.handleCount)
+	rs.openRequests[r.handle] = r
+
+	return r.handle
 }
 
 // Returns Request from openRequests, bool is false if it is missing.
@@ -84,20 +90,23 @@
 // you can do different things with. What you are doing with it are denoted by
 // the first packet of that type (read/write/etc).
 func (rs *RequestServer) getRequest(handle string) (*Request, bool) {
-	rs.openRequestLock.RLock()
-	defer rs.openRequestLock.RUnlock()
+	rs.mu.RLock()
+	defer rs.mu.RUnlock()
+
 	r, ok := rs.openRequests[handle]
 	return r, ok
 }
 
 // Close the Request and clear from openRequests map
 func (rs *RequestServer) closeRequest(handle string) error {
-	rs.openRequestLock.Lock()
-	defer rs.openRequestLock.Unlock()
+	rs.mu.Lock()
+	defer rs.mu.Unlock()
+
 	if r, ok := rs.openRequests[handle]; ok {
 		delete(rs.openRequests, handle)
 		return r.close()
 	}
+
 	return EBADF
 }
 
@@ -142,8 +151,10 @@
 			rs.pktMgr.alloc.Free()
 		}
 	}()
+
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
+
 	var wg sync.WaitGroup
 	runWorker := func(ch chan orderedRequest) {
 		wg.Add(1)
@@ -160,8 +171,8 @@
 
 	wg.Wait() // wait for all workers to exit
 
-	rs.openRequestLock.Lock()
-	defer rs.openRequestLock.Unlock()
+	rs.mu.Lock()
+	defer rs.mu.Unlock()
 
 	// make sure all open requests are properly closed
 	// (eg. possible on dropped connections, client crashes, etc.)
@@ -178,9 +189,7 @@
 	return err
 }
 
-func (rs *RequestServer) packetWorker(
-	ctx context.Context, pktChan chan orderedRequest,
-) error {
+func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedRequest) error {
 	for pkt := range pktChan {
 		orderID := pkt.orderID()
 		if epkt, ok := pkt.requestPacket.(*sshFxpExtendedPacket); ok {
diff --git a/request-server_test.go b/request-server_test.go
index ddb6129..88862fb 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -440,9 +440,9 @@
 	require.NoError(t, err)
 	fi, err := p.cli.Stat("/foo")
 	require.NoError(t, err)
-	assert.Equal(t, fi.Name(), "foo")
-	assert.Equal(t, fi.Size(), int64(5))
-	assert.Equal(t, fi.Mode(), os.FileMode(0644))
+	assert.Equal(t, "foo", fi.Name())
+	assert.Equal(t, int64(5), fi.Size())
+	assert.Equal(t, os.FileMode(0644), fi.Mode())
 	assert.NoError(t, testOsSys(fi.Sys()))
 	checkRequestServerAllocator(t, p)
 }
@@ -459,9 +459,9 @@
 	require.NoError(t, err)
 	fi, err := p.cli.Stat("/foo")
 	require.NoError(t, err)
-	assert.Equal(t, fi.Name(), "foo")
-	assert.Equal(t, fi.Size(), int64(5))
-	assert.Equal(t, fi.Mode(), os.FileMode(0644))
+	assert.Equal(t, "foo", fi.Name())
+	assert.Equal(t, int64(5), fi.Size())
+	assert.Equal(t, os.FileMode(0644), fi.Mode())
 	assert.NoError(t, testOsSys(fi.Sys()))
 	checkRequestServerAllocator(t, p)
 }
@@ -475,9 +475,9 @@
 	require.NoError(t, err)
 	fi, err := fp.Stat()
 	require.NoError(t, err)
-	assert.Equal(t, fi.Name(), "foo")
-	assert.Equal(t, fi.Size(), int64(5))
-	assert.Equal(t, fi.Mode(), os.FileMode(0644))
+	assert.Equal(t, "foo", fi.Name())
+	assert.Equal(t, int64(5), fi.Size())
+	assert.Equal(t, os.FileMode(0644), fi.Mode())
 	assert.NoError(t, testOsSys(fi.Sys()))
 	checkRequestServerAllocator(t, p)
 }
@@ -490,6 +490,7 @@
 	fp, err := p.cli.OpenFile("/foo", os.O_WRONLY)
 	require.NoError(t, err)
 	err = fp.Truncate(2)
+	require.NoError(t, err)
 	fi, err := fp.Stat()
 	require.NoError(t, err)
 	assert.Equal(t, fi.Name(), "foo")
diff --git a/request.go b/request.go
index d842787..c6da4b6 100644
--- a/request.go
+++ b/request.go
@@ -505,10 +505,14 @@
 
 		nameAttrs := make([]*sshFxpNameAttr, 0, len(finfo))
 
+		// If the type conversion fails, we get untyped `nil`,
+		// which is handled by not looking up any names.
+		idLookup, _ := h.(NameLookupFileLister)
+
 		for _, fi := range finfo {
 			nameAttrs = append(nameAttrs, &sshFxpNameAttr{
 				Name:     fi.Name(),
-				LongName: runLs(fi),
+				LongName: runLs(idLookup, fi),
 				Attrs:    []interface{}{fi},
 			})
 		}
diff --git a/server.go b/server.go
index a38d09b..529052b 100644
--- a/server.go
+++ b/server.go
@@ -466,11 +466,13 @@
 		return statusFromError(p.ID, err)
 	}
 
+	idLookup := osIDLookup{}
+
 	ret := &sshFxpNamePacket{ID: p.ID}
 	for _, dirent := range dirents {
 		ret.NameAttrs = append(ret.NameAttrs, &sshFxpNameAttr{
 			Name:     dirent.Name(),
-			LongName: runLs(dirent),
+			LongName: runLs(idLookup, dirent),
 			Attrs:    []interface{}{dirent},
 		})
 	}
@@ -612,99 +614,3 @@
 
 	return ret
 }
-
-func clamp(v, max uint32) uint32 {
-	if v > max {
-		return max
-	}
-	return v
-}
-
-func runLsTypeWord(dirent os.FileInfo) string {
-	// find first character, the type char
-	// b     Block special file.
-	// c     Character special file.
-	// d     Directory.
-	// l     Symbolic link.
-	// s     Socket link.
-	// p     FIFO.
-	// -     Regular file.
-	tc := '-'
-	mode := dirent.Mode()
-	if (mode & os.ModeDir) != 0 {
-		tc = 'd'
-	} else if (mode & os.ModeDevice) != 0 {
-		tc = 'b'
-		if (mode & os.ModeCharDevice) != 0 {
-			tc = 'c'
-		}
-	} else if (mode & os.ModeSymlink) != 0 {
-		tc = 'l'
-	} else if (mode & os.ModeSocket) != 0 {
-		tc = 's'
-	} else if (mode & os.ModeNamedPipe) != 0 {
-		tc = 'p'
-	}
-
-	// owner
-	orc := '-'
-	if (mode & 0400) != 0 {
-		orc = 'r'
-	}
-	owc := '-'
-	if (mode & 0200) != 0 {
-		owc = 'w'
-	}
-	oxc := '-'
-	ox := (mode & 0100) != 0
-	setuid := (mode & os.ModeSetuid) != 0
-	if ox && setuid {
-		oxc = 's'
-	} else if setuid {
-		oxc = 'S'
-	} else if ox {
-		oxc = 'x'
-	}
-
-	// group
-	grc := '-'
-	if (mode & 040) != 0 {
-		grc = 'r'
-	}
-	gwc := '-'
-	if (mode & 020) != 0 {
-		gwc = 'w'
-	}
-	gxc := '-'
-	gx := (mode & 010) != 0
-	setgid := (mode & os.ModeSetgid) != 0
-	if gx && setgid {
-		gxc = 's'
-	} else if setgid {
-		gxc = 'S'
-	} else if gx {
-		gxc = 'x'
-	}
-
-	// all / others
-	arc := '-'
-	if (mode & 04) != 0 {
-		arc = 'r'
-	}
-	awc := '-'
-	if (mode & 02) != 0 {
-		awc = 'w'
-	}
-	axc := '-'
-	ax := (mode & 01) != 0
-	sticky := (mode & os.ModeSticky) != 0
-	if ax && sticky {
-		axc = 't'
-	} else if sticky {
-		axc = 'T'
-	} else if ax {
-		axc = 'x'
-	}
-
-	return fmt.Sprintf("%c%c%c%c%c%c%c%c%c%c", tc, orc, owc, oxc, grc, gwc, gxc, arc, awc, axc)
-}
diff --git a/server_stubs.go b/server_stubs.go
deleted file mode 100644
index 84a7d7f..0000000
--- a/server_stubs.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// +build !cgo plan9 windows android
-
-package sftp
-
-import (
-	"fmt"
-	"os"
-	"time"
-)
-
-func runLs(dirent os.FileInfo) string {
-	typeword := runLsTypeWord(dirent)
-	numLinks := 1
-	if dirent.IsDir() {
-		numLinks = 0
-	}
-	username := "root"
-	groupname := "root"
-	mtime := dirent.ModTime()
-	monthStr := mtime.Month().String()[0:3]
-	day := mtime.Day()
-	year := mtime.Year()
-	now := time.Now()
-	isOld := mtime.Before(now.Add(-time.Hour * 24 * 365 / 2))
-
-	yearOrTime := fmt.Sprintf("%02d:%02d", mtime.Hour(), mtime.Minute())
-	if isOld {
-		yearOrTime = fmt.Sprintf("%d", year)
-	}
-
-	return fmt.Sprintf("%s %4d %-8s %-8s %8d %s %2d %5s %s", typeword, numLinks, username, groupname, dirent.Size(), monthStr, day, yearOrTime, dirent.Name())
-}
diff --git a/server_test.go b/server_test.go
index 74da0c6..ae61eec 100644
--- a/server_test.go
+++ b/server_test.go
@@ -6,162 +6,15 @@
 	"io"
 	"os"
 	"path"
-	"regexp"
 	"runtime"
 	"sync"
 	"syscall"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-const (
-	typeDirectory = "d"
-	typeFile      = "[^d]"
-)
-
-func TestRunLsWithExamplesDirectory(t *testing.T) {
-	path := "examples"
-	item, _ := os.Stat(path)
-	result := runLs(item)
-	runLsTestHelper(t, result, typeDirectory, path)
-}
-
-func TestRunLsWithLicensesFile(t *testing.T) {
-	path := "LICENSE"
-	item, _ := os.Stat(path)
-	result := runLs(item)
-	runLsTestHelper(t, result, typeFile, path)
-}
-
-/*
-   The format of the `longname' field is unspecified by this protocol.
-   It MUST be suitable for use in the output of a directory listing
-   command (in fact, the recommended operation for a directory listing
-   command is to simply display this data).  However, clients SHOULD NOT
-   attempt to parse the longname field for file attributes; they SHOULD
-   use the attrs field instead.
-
-    The recommended format for the longname field is as follows:
-
-        -rwxr-xr-x   1 mjos     staff      348911 Mar 25 14:29 t-filexfer
-        1234567890 123 12345678 12345678 12345678 123456789012
-
-   Here, the first line is sample output, and the second field indicates
-   widths of the various fields.  Fields are separated by spaces.  The
-   first field lists file permissions for user, group, and others; the
-   second field is link count; the third field is the name of the user
-   who owns the file; the fourth field is the name of the group that
-   owns the file; the fifth field is the size of the file in bytes; the
-   sixth field (which actually may contain spaces, but is fixed to 12
-   characters) is the file modification time, and the seventh field is
-   the file name.  Each field is specified to be a minimum of certain
-   number of character positions (indicated by the second line above),
-   but may also be longer if the data does not fit in the specified
-   length.
-
-    The SSH_FXP_ATTRS response has the following format:
-
-        uint32     id
-        ATTRS      attrs
-
-   where `id' is the request identifier, and `attrs' is the returned
-   file attributes as described in Section ``File Attributes''.
-*/
-func runLsTestHelper(t *testing.T, result, expectedType, path string) {
-	// using regular expressions to make tests work on all systems
-	// a virtual file system (like afero) would be needed to mock valid filesystem checks
-	// expected layout is:
-	// drwxr-xr-x   8 501      20            272 Aug  9 19:46 examples
-
-	// permissions (len 10, "drwxr-xr-x")
-	got := result[0:10]
-	if ok, err := regexp.MatchString("^"+expectedType+"[rwx-]{9}$", got); !ok {
-		t.Errorf("runLs(*FileInfo): permission field mismatch, expected dir, got: %#v, err: %#v", got, err)
-	}
-
-	// space
-	got = result[10:11]
-	if ok, err := regexp.MatchString("^\\s$", got); !ok {
-		t.Errorf("runLs(*FileInfo): spacer 1 mismatch, expected whitespace, got: %#v, err: %#v", got, err)
-	}
-
-	// link count (len 3, number)
-	got = result[12:15]
-	if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
-		t.Errorf("runLs(*FileInfo): link count field mismatch, got: %#v, err: %#v", got, err)
-	}
-
-	// spacer
-	got = result[15:16]
-	if ok, err := regexp.MatchString("^\\s$", got); !ok {
-		t.Errorf("runLs(*FileInfo): spacer 2 mismatch, expected whitespace, got: %#v, err: %#v", got, err)
-	}
-
-	// username / uid (len 8, number or string)
-	got = result[16:24]
-	if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok {
-		t.Errorf("runLs(*FileInfo): username / uid mismatch, expected user, got: %#v, err: %#v", got, err)
-	}
-
-	// spacer
-	got = result[24:25]
-	if ok, err := regexp.MatchString("^\\s$", got); !ok {
-		t.Errorf("runLs(*FileInfo): spacer 3 mismatch, expected whitespace, got: %#v, err: %#v", got, err)
-	}
-
-	// groupname / gid (len 8, number or string)
-	got = result[25:33]
-	if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok {
-		t.Errorf("runLs(*FileInfo): groupname / gid mismatch, expected group, got: %#v, err: %#v", got, err)
-	}
-
-	// spacer
-	got = result[33:34]
-	if ok, err := regexp.MatchString("^\\s$", got); !ok {
-		t.Errorf("runLs(*FileInfo): spacer 4 mismatch, expected whitespace, got: %#v, err: %#v", got, err)
-	}
-
-	// filesize (len 8)
-	got = result[34:42]
-	if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
-		t.Errorf("runLs(*FileInfo): filesize field mismatch, expected size in bytes, got: %#v, err: %#v", got, err)
-	}
-
-	// spacer
-	got = result[42:43]
-	if ok, err := regexp.MatchString("^\\s$", got); !ok {
-		t.Errorf("runLs(*FileInfo): spacer 5 mismatch, expected whitespace, got: %#v, err: %#v", got, err)
-	}
-
-	// mod time (len 12, e.g. Aug  9 19:46)
-	got = result[43:55]
-	layout := "Jan  2 15:04"
-	_, err := time.Parse(layout, got)
-
-	if err != nil {
-		layout = "Jan  2 2006"
-		_, err = time.Parse(layout, got)
-	}
-	if err != nil {
-		t.Errorf("runLs(*FileInfo): mod time field mismatch, expected date layout %s, got: %#v, err: %#v", layout, got, err)
-	}
-
-	// spacer
-	got = result[55:56]
-	if ok, err := regexp.MatchString("^\\s$", got); !ok {
-		t.Errorf("runLs(*FileInfo): spacer 6 mismatch, expected whitespace, got: %#v, err: %#v", got, err)
-	}
-
-	// filename
-	got = result[56:]
-	if ok, err := regexp.MatchString("^"+path+"$", got); !ok {
-		t.Errorf("runLs(*FileInfo): name field mismatch, expected examples, got: %#v, err: %#v", got, err)
-	}
-}
-
 func clientServerPair(t *testing.T) (*Client, *Server) {
 	cr, sw := io.Pipe()
 	sr, cw := io.Pipe()
diff --git a/server_unix.go b/server_unix.go
deleted file mode 100644
index 9ba54b6..0000000
--- a/server_unix.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix
-// +build cgo
-
-package sftp
-
-import (
-	"fmt"
-	"os"
-	"syscall"
-	"time"
-)
-
-// ls -l style output for a file, which is in the 'long output' section of a readdir response packet
-// this is a very simple (lazy) implementation, just enough to look almost like openssh in a few basic cases
-func runLs(dirent os.FileInfo) string {
-	// example from openssh sftp server:
-	// crw-rw-rw-    1 root     wheel           0 Jul 31 20:52 ttyvd
-	// format:
-	// {directory / char device / etc}{rwxrwxrwx}  {number of links} owner group size month day [time (this year) | year (otherwise)] name
-
-	typeword := runLsTypeWord(dirent)
-
-	var numLinks uint64 = 1
-	if dirent.IsDir() {
-		numLinks = 0
-	}
-
-	var uid, gid uint32
-
-	if statt, ok := dirent.Sys().(*syscall.Stat_t); ok {
-		// The type of Nlink varies form int16 (aix-ppc64) to uint64 (linux-amd64),
-		// we cast up to uint64 to make all OS/ARCH combos source compatible.
-		numLinks = uint64(statt.Nlink)
-		uid = statt.Uid
-		gid = statt.Gid
-	}
-
-	username := fmt.Sprintf("%d", uid)
-	groupname := fmt.Sprintf("%d", gid)
-	// TODO FIXME: uid -> username, gid -> groupname lookup for ls -l format output
-
-	mtime := dirent.ModTime()
-	monthStr := mtime.Month().String()[0:3]
-	day := mtime.Day()
-	year := mtime.Year()
-	now := time.Now()
-	isOld := mtime.Before(now.Add(-time.Hour * 24 * 365 / 2))
-
-	yearOrTime := fmt.Sprintf("%02d:%02d", mtime.Hour(), mtime.Minute())
-	if isOld {
-		yearOrTime = fmt.Sprintf("%d", year)
-	}
-
-	return fmt.Sprintf("%s %4d %-8s %-8s %8d %s %2d %5s %s", typeword, numLinks, username, groupname, dirent.Size(), monthStr, day, yearOrTime, dirent.Name())
-}
diff --git a/sftp_test.go b/sftp_test.go
index 5fe2bd4..487b84d 100644
--- a/sftp_test.go
+++ b/sftp_test.go
@@ -70,5 +70,6 @@
 	assert.Equal(t, expectedSFTPExtensions, sftpExtensions)
 
 	err = SetSFTPExtensions(supportedExtensions...)
+	assert.NoError(t, err)
 	assert.Equal(t, supportedSFTPExtensions, sftpExtensions)
 }
diff --git a/stat_plan9.go b/stat_plan9.go
index 52da020..761abdf 100644
--- a/stat_plan9.go
+++ b/stat_plan9.go
@@ -49,6 +49,7 @@
 // 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
@@ -65,38 +66,31 @@
 	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(0)
+	ret := uint32(mode & os.ModePerm)
 
-	if mode&os.ModeDevice != 0 {
-		if mode&os.ModeCharDevice != 0 {
-			ret |= syscall.S_IFCHR
-		} else {
-			ret |= syscall.S_IFBLK
-		}
-	}
-	if mode&os.ModeDir != 0 {
+	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
-	}
-	if mode&os.ModeSymlink != 0 {
-		ret |= syscall.S_IFLNK
-	}
-	if mode&os.ModeNamedPipe != 0 {
+	case os.ModeNamedPipe:
 		ret |= syscall.S_IFIFO
-	}
-	if mode&os.ModeSocket != 0 {
+	case os.ModeSymlink:
+		ret |= syscall.S_IFLNK
+	case 0:
+		ret |= syscall.S_IFREG
+	case os.ModeSocket:
 		ret |= syscall.S_IFSOCK
 	}
 
-	if mode&os.ModeType == 0 {
-		ret |= syscall.S_IFREG
-	}
-	ret |= uint32(mode & os.ModePerm)
-
 	return ret
 }
 
diff --git a/stat_posix.go b/stat_posix.go
index 98b60e7..92f76e2 100644
--- a/stat_posix.go
+++ b/stat_posix.go
@@ -51,6 +51,7 @@
 // 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
@@ -67,55 +68,50 @@
 	case syscall.S_IFSOCK:
 		fm |= os.ModeSocket
 	}
-	if mode&syscall.S_ISGID != 0 {
-		fm |= os.ModeSetgid
-	}
+
 	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(0)
+	ret := uint32(mode & os.ModePerm)
 
-	if mode&os.ModeDevice != 0 {
-		if mode&os.ModeCharDevice != 0 {
-			ret |= syscall.S_IFCHR
-		} else {
-			ret |= syscall.S_IFBLK
-		}
-	}
-	if mode&os.ModeDir != 0 {
+	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
-	}
-	if mode&os.ModeSymlink != 0 {
-		ret |= syscall.S_IFLNK
-	}
-	if mode&os.ModeNamedPipe != 0 {
+	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.ModeSetuid != 0 {
-		ret |= syscall.S_ISUID
-	}
 	if mode&os.ModeSticky != 0 {
 		ret |= syscall.S_ISVTX
 	}
-	if mode&os.ModeSocket != 0 {
-		ret |= syscall.S_IFSOCK
-	}
-
-	if mode&os.ModeType == 0 {
-		ret |= syscall.S_IFREG
-	}
-	ret |= uint32(mode & os.ModePerm)
 
 	return ret
 }