Merge branch 'master' into master
diff --git a/packet-manager.go b/packet-manager.go
index 367d75e..906b3ee 100644
--- a/packet-manager.go
+++ b/packet-manager.go
@@ -147,10 +147,10 @@
 	}
 }
 
-func outfilter(o []responsePacket) []uint32 {
-	res := make([]uint32, 0, len(o))
-	for _, v := range o {
-		res = append(res, v.id())
-	}
-	return res
-}
+//func outfilter(o []responsePacket) []uint32 {
+//	res := make([]uint32, 0, len(o))
+//	for _, v := range o {
+//		res = append(res, v.id())
+//	}
+//	return res
+//}
diff --git a/request-server.go b/request-server.go
index b0cad92..225914f 100644
--- a/request-server.go
+++ b/request-server.go
@@ -10,6 +10,7 @@
 	"syscall"
 
 	"github.com/pkg/errors"
+	"strings"
 )
 
 var maxTxPacket uint32 = 1 << 15
@@ -130,7 +131,7 @@
 			rs.closeRequest(handle)
 			rpkt = statusFromError(pkt, nil)
 		case *sshFxpRealpathPacket:
-			rpkt = cleanPath(pkt)
+			rpkt = cleanPacketPath(pkt)
 		case isOpener:
 			handle := rs.nextRequest(requestFromPacket(pkt))
 			rpkt = sshFxpHandlePacket{pkt.id(), handle}
@@ -180,23 +181,26 @@
 	return nil
 }
 
-func cleanPath(pkt *sshFxpRealpathPacket) responsePacket {
-	path := pkt.getPath()
-	if !filepath.IsAbs(path) {
-		path = "/" + path
-	} // all paths are absolute
-
-	cleaned_path := filepath.Clean(path)
+func cleanPacketPath(pkt *sshFxpRealpathPacket) responsePacket {
+	path := cleanPath(pkt.getPath())
 	return &sshFxpNamePacket{
 		ID: pkt.id(),
 		NameAttrs: []sshFxpNameAttr{{
-			Name:     cleaned_path,
-			LongName: cleaned_path,
+			Name:     path,
+			LongName: path,
 			Attrs:    emptyFileStat,
 		}},
 	}
 }
 
+func cleanPath(path string) string {
+	cleanSlashPath := filepath.ToSlash(filepath.Clean(path))
+	if !strings.HasPrefix(cleanSlashPath, "/") {
+		return "/" + cleanSlashPath
+	}
+	return cleanSlashPath
+}
+
 func (rs *RequestServer) handle(request Request, pkt requestPacket) responsePacket {
 	// fmt.Println("Request Method: ", request.Method)
 	rpkt, err := request.handle(rs.Handlers)
diff --git a/request-server_test.go b/request-server_test.go
index 60aed2a..0c25ecf 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -328,3 +328,20 @@
 	names := []string{di[18].Name(), di[81].Name()}
 	assert.Equal(t, []string{"foo_18", "foo_81"}, names)
 }
+
+func TestCleanPath(t *testing.T) {
+	assert.Equal(t, "/", cleanPath("/"))
+	assert.Equal(t, "/", cleanPath("//"))
+	assert.Equal(t, "/a", cleanPath("/a/"))
+	assert.Equal(t, "/a", cleanPath("a/"))
+	assert.Equal(t, "/a/b/c", cleanPath("/a//b//c/"))
+
+	// filepath.ToSlash does not touch \ as char on unix systems, so os.PathSeparator is used for windows compatible tests
+	bslash := string(os.PathSeparator)
+	assert.Equal(t, "/", cleanPath(bslash))
+	assert.Equal(t, "/", cleanPath(bslash+bslash))
+	assert.Equal(t, "/a", cleanPath(bslash+"a"+bslash))
+	assert.Equal(t, "/a", cleanPath("a"+bslash))
+	assert.Equal(t, "/a/b/c", cleanPath(bslash+"a"+bslash+bslash+"b"+bslash+bslash+"c"+bslash))
+
+}
diff --git a/request.go b/request.go
index 906e9ce..3e739f9 100644
--- a/request.go
+++ b/request.go
@@ -56,16 +56,16 @@
 		request.Flags = p.Flags
 		request.Attrs = p.Attrs.([]byte)
 	case *sshFxpRenamePacket:
-		request.Target = filepath.Clean(p.Newpath)
+		request.Target = cleanPath(p.Newpath)
 	case *sshFxpSymlinkPacket:
-		request.Target = filepath.Clean(p.Linkpath)
+		request.Target = cleanPath(p.Linkpath)
 	}
 	return request
 }
 
 // NewRequest creates a new Request object.
 func NewRequest(method, path string) Request {
-	request := Request{Method: method, Filepath: filepath.Clean(path)}
+	request := Request{Method: method, Filepath: cleanPath(path)}
 	request.packets = make(chan packet_data, SftpServerWorkerCount)
 	request.state = &state{}
 	request.stateLock = &sync.RWMutex{}
@@ -269,7 +269,7 @@
 	switch r.Method {
 	case "List":
 		pd := r.popPacket()
-		dirname := path.Base(r.Filepath)
+		dirname := filepath.ToSlash(path.Base(r.Filepath))
 		ret := &sshFxpNamePacket{ID: pd.id}
 
 		for _, fi := range finfo {
diff --git a/request_test.go b/request_test.go
index efcedcd..33b53e1 100644
--- a/request_test.go
+++ b/request_test.go
@@ -70,8 +70,8 @@
 		stateLock: &sync.RWMutex{},
 	}
 	for _, p := range []packet_data{
-		packet_data{id: 1, data: filecontents[:5], length: 5},
-		packet_data{id: 2, data: filecontents[5:], length: 5, offset: 5}} {
+		{id: 1, data: filecontents[:5], length: 5},
+		{id: 2, data: filecontents[5:], length: 5, offset: 5}} {
 		request.packets <- p
 	}
 	return request
diff --git a/server.go b/server.go
index 13fd7ff..80a1288 100644
--- a/server.go
+++ b/server.go
@@ -230,8 +230,7 @@
 		if err != nil {
 			return s.sendError(p, err)
 		}
-		f = filepath.Clean(f)
-		f = filepath.ToSlash(f) // make path more Unix like on windows servers
+		f = cleanPath(f)
 		return s.sendPacket(sshFxpNamePacket{
 			ID: p.ID,
 			NameAttrs: []sshFxpNameAttr{{
@@ -573,3 +572,92 @@
 	}
 	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
index 3b1ddbd..a14c734 100644
--- a/server_stubs.go
+++ b/server_stubs.go
@@ -4,9 +4,29 @@
 
 import (
 	"os"
-	"path"
+	"time"
+	"fmt"
 )
 
 func runLs(dirname string, dirent os.FileInfo) string {
-	return path.Join(dirname, dirent.Name())
+	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 721acc6..f2f034a 100644
--- a/server_test.go
+++ b/server_test.go
@@ -1,11 +1,159 @@
 package sftp
 
 import (
+	"testing"
+	"os"
+	"regexp"
+	"time"
 	"io"
 	"sync"
-	"testing"
 )
 
+const (
+	typeDirectory = "d"
+	typeFile      = "[^d]"
+)
+
+func TestRunLsWithExamplesDirectory(t *testing.T) {
+	path := "examples"
+	item, _ := os.Stat(path)
+	result := runLs(path, item)
+	runLsTestHelper(t, result, typeDirectory, path)
+}
+
+func TestRunLsWithLicensesFile(t *testing.T) {
+	path := "LICENSE"
+	item, _ := os.Stat(path)
+	result := runLs(path, 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(%#v, *FileInfo): permission field mismatch, expected dir, got: %#v, err: %#v", path, got, err)
+	}
+
+	// space
+	got = result[10:11]
+	if ok, err := regexp.MatchString("^\\s$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): spacer 1 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+	}
+
+	// link count (len 3, number)
+	got = result[12:15]
+	if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): link count field mismatch, got: %#v, err: %#v", path, got, err)
+	}
+
+	// spacer
+	got = result[15:16]
+	if ok, err := regexp.MatchString("^\\s$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): spacer 2 mismatch, expected whitespace, got: %#v, err: %#v", path, 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(%#v, *FileInfo): username / uid mismatch, expected user, got: %#v, err: %#v", path, got, err)
+	}
+
+	// spacer
+	got = result[24:25]
+	if ok, err := regexp.MatchString("^\\s$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): spacer 3 mismatch, expected whitespace, got: %#v, err: %#v", path, 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(%#v, *FileInfo): groupname / gid mismatch, expected group, got: %#v, err: %#v", path, got, err)
+	}
+
+	// spacer
+	got = result[33:34]
+	if ok, err := regexp.MatchString("^\\s$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): spacer 4 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+	}
+
+	// filesize (len 8)
+	got = result[34:42]
+	if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): filesize field mismatch, expected size in bytes, got: %#v, err: %#v", path, got, err)
+	}
+
+	// spacer
+	got = result[42:43]
+	if ok, err := regexp.MatchString("^\\s$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): spacer 5 mismatch, expected whitespace, got: %#v, err: %#v", path, 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(%#v, *FileInfo): mod time field mismatch, expected date layout %s, got: %#v, err: %#v", path, layout, got, err)
+	}
+
+	// spacer
+	got = result[55:56]
+	if ok, err := regexp.MatchString("^\\s$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): spacer 6 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+	}
+
+	// filename
+	got = result[56:]
+	if ok, err := regexp.MatchString("^"+path+"$", got); !ok {
+		t.Errorf("runLs(%#v, *FileInfo): name field mismatch, expected examples, got: %#v, err: %#v", path, 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
index 8c3f0b4..8d0c138 100644
--- a/server_unix.go
+++ b/server_unix.go
@@ -11,96 +11,7 @@
 	"time"
 )
 
-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)
-}
-
-func runLsStatt(dirname string, dirent os.FileInfo, statt *syscall.Stat_t) string {
+func runLsStatt(dirent os.FileInfo, statt *syscall.Stat_t) string {
 	// example from openssh sftp server:
 	// crw-rw-rw-    1 root     wheel           0 Jul 31 20:52 ttyvd
 	// format:
@@ -136,7 +47,7 @@
 	if dsys == nil {
 	} else if statt, ok := dsys.(*syscall.Stat_t); !ok {
 	} else {
-		return runLsStatt(dirname, dirent, statt)
+		return runLsStatt(dirent, statt)
 	}
 
 	return path.Join(dirname, dirent.Name())