Repository: pkg/sftp
Pull request #553 (2023-07-13 08:57:53)

Marshal extended data if file info supports it
If the file information returned by ListAt supports the FileInfoExtendedData interface, use it to retrieve extended data and pass it back to the client. In similar fashion, we add the FileInfoUidGid interface to allow for implementations to return uid and gid data, even if the os lacks support in syscall.Stat_t.


Patch:
diff --git a/attrs.go b/attrs.go
index cb0987d..894d080 100644
--- a/attrs.go
+++ b/attrs.go
@@ -1,7 +1,7 @@
 package sftp
 
 // ssh_FXP_ATTRS support
-// see https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
+// see http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
 
 import (
 	"os"
@@ -69,6 +69,20 @@ func fileInfoFromStat(stat *FileStat, name string) os.FileInfo {
 	}
 }
 
+// FileInfoUidGid extends os.FileInfo and adds callbacks for Uid and Gid retrieval,
+// as an alternative to *syscall.Stat_t objects on unix systems.
+type FileInfoUidGid interface {
+	os.FileInfo
+	Uid() uint32
+	Gid() uint32
+}
+
+// FileInfoUidGid extends os.FileInfo and adds a callbacks for extended data retrieval.
+type FileInfoExtendedData interface {
+	os.FileInfo
+	Extended() []StatExtended
+}
+
 func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) {
 	mtime := fi.ModTime().Unix()
 	atime := mtime
@@ -86,5 +100,22 @@ func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) {
 	// os specific file stat decoding
 	fileStatFromInfoOs(fi, &flags, fileStat)
 
+	// The call above will include the sshFileXferAttrUIDGID in case
+	// the os.FileInfo can be casted to *syscall.Stat_t on unix.
+	// If fi implements FileInfoUidGid, retrieve Uid, Gid from it instead.
+	if fiExt, ok := fi.(FileInfoUidGid); ok {
+		flags |= sshFileXferAttrUIDGID
+		fileStat.UID = fiExt.Uid()
+		fileStat.GID = fiExt.Gid()
+	}
+
+	// if fi implements FileInfoExtendedData, retrieve extended data from it
+	if fiExt, ok := fi.(FileInfoExtendedData); ok {
+		fileStat.Extended = fiExt.Extended()
+		if len(fileStat.Extended) > 0 {
+			flags |= sshFileXferAttrExtended
+		}
+	}
+
 	return flags, fileStat
 }
diff --git a/attrs_stubs.go b/attrs_stubs.go
index d20348c..c01f336 100644
--- a/attrs_stubs.go
+++ b/attrs_stubs.go
@@ -1,4 +1,3 @@
-//go:build plan9 || windows || android
 // +build plan9 windows android
 
 package sftp
diff --git a/attrs_unix.go b/attrs_unix.go
index 371ae9b..d1f4452 100644
--- a/attrs_unix.go
+++ b/attrs_unix.go
@@ -1,4 +1,3 @@
-//go:build darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || aix || js
 // +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js
 
 package sftp
diff --git a/client.go b/client.go
index 0df125e..975bb7a 100644
--- a/client.go
+++ b/client.go
@@ -259,11 +259,11 @@ func (c *Client) Create(path string) (*File, error) {
 	return c.open(path, flags(os.O_RDWR|os.O_CREATE|os.O_TRUNC))
 }
 
-const sftpProtocolVersion = 3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
+const sftpProtocolVersion = 3 // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
 
 func (c *Client) sendInit() error {
 	return c.clientConn.conn.sendPacket(&sshFxInitPacket{
-		Version: sftpProtocolVersion, // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
+		Version: sftpProtocolVersion, // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
 	})
 }
 
@@ -924,45 +924,6 @@ func (c *Client) MkdirAll(path string) error {
 	return nil
 }
 
-// RemoveAll delete files recursively in the directory and Recursively delete subdirectories.
-// An error will be returned if no file or directory with the specified path exists
-func (c *Client) RemoveAll(path string) error {
-
-	// Get the file/directory information
-	fi, err := c.Stat(path)
-	if err != nil {
-		return err
-	}
-
-	if fi.IsDir() {
-		// Delete files recursively in the directory
-		files, err := c.ReadDir(path)
-		if err != nil {
-			return err
-		}
-
-		for _, file := range files {
-			if file.IsDir() {
-				// Recursively delete subdirectories
-				err = c.RemoveAll(path + "/" + file.Name())
-				if err != nil {
-					return err
-				}
-			} else {
-				// Delete individual files
-				err = c.Remove(path + "/" + file.Name())
-				if err != nil {
-					return err
-				}
-			}
-		}
-
-	}
-
-	return c.Remove(path)
-
-}
-
 // File represents a remote file.
 type File struct {
 	c      *Client
diff --git a/debug.go b/debug.go
index f0db14d..3e264ab 100644
--- a/debug.go
+++ b/debug.go
@@ -1,4 +1,3 @@
-//go:build debug
 // +build debug
 
 package sftp
diff --git a/fuzz.go b/fuzz.go
index f2f1fc3..169aebc 100644
--- a/fuzz.go
+++ b/fuzz.go
@@ -1,4 +1,3 @@
-//go:build gofuzz
 // +build gofuzz
 
 package sftp
diff --git a/internal/encoding/ssh/filexfer/attrs.go b/internal/encoding/ssh/filexfer/attrs.go
index 3aec937..eed61bf 100644
--- a/internal/encoding/ssh/filexfer/attrs.go
+++ b/internal/encoding/ssh/filexfer/attrs.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // Attributes related flags.
 const (
@@ -12,7 +12,7 @@ const (
 
 // Attributes defines the file attributes type defined in draft-ietf-secsh-filexfer-02
 //
-// Defined in: https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
+// Defined in: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
 type Attributes struct {
 	Flags uint32
 
@@ -116,32 +116,32 @@ func (a *Attributes) Len() int {
 }
 
 // MarshalInto marshals e onto the end of the given Buffer.
-func (a *Attributes) MarshalInto(buf *Buffer) {
-	buf.AppendUint32(a.Flags)
+func (a *Attributes) MarshalInto(b *Buffer) {
+	b.AppendUint32(a.Flags)
 
 	if a.Flags&AttrSize != 0 {
-		buf.AppendUint64(a.Size)
+		b.AppendUint64(a.Size)
 	}
 
 	if a.Flags&AttrUIDGID != 0 {
-		buf.AppendUint32(a.UID)
-		buf.AppendUint32(a.GID)
+		b.AppendUint32(a.UID)
+		b.AppendUint32(a.GID)
 	}
 
 	if a.Flags&AttrPermissions != 0 {
-		buf.AppendUint32(uint32(a.Permissions))
+		b.AppendUint32(uint32(a.Permissions))
 	}
 
 	if a.Flags&AttrACModTime != 0 {
-		buf.AppendUint32(a.ATime)
-		buf.AppendUint32(a.MTime)
+		b.AppendUint32(a.ATime)
+		b.AppendUint32(a.MTime)
 	}
 
 	if a.Flags&AttrExtended != 0 {
-		buf.AppendUint32(uint32(len(a.ExtendedAttributes)))
+		b.AppendUint32(uint32(len(a.ExtendedAttributes)))
 
 		for _, ext := range a.ExtendedAttributes {
-			ext.MarshalInto(buf)
+			ext.MarshalInto(b)
 		}
 	}
 }
@@ -156,51 +156,74 @@ func (a *Attributes) MarshalBinary() ([]byte, error) {
 // UnmarshalFrom unmarshals an Attributes from the given Buffer into e.
 //
 // NOTE: The values of fields not covered in the a.Flags are explicitly undefined.
-func (a *Attributes) UnmarshalFrom(buf *Buffer) (err error) {
-	flags := buf.ConsumeUint32()
+func (a *Attributes) UnmarshalFrom(b *Buffer) (err error) {
+	flags, err := b.ConsumeUint32()
+	if err != nil {
+		return err
+	}
 
-	return a.XXX_UnmarshalByFlags(flags, buf)
+	return a.XXX_UnmarshalByFlags(flags, b)
 }
 
 // XXX_UnmarshalByFlags uses the pre-existing a.Flags field to determine which fields to decode.
 // DO NOT USE THIS: it is an anti-corruption function to implement existing internal usage in pkg/sftp.
 // This function is not a part of any compatibility promise.
-func (a *Attributes) XXX_UnmarshalByFlags(flags uint32, buf *Buffer) (err error) {
+func (a *Attributes) XXX_UnmarshalByFlags(flags uint32, b *Buffer) (err error) {
 	a.Flags = flags
 
 	// Short-circuit dummy attributes.
 	if a.Flags == 0 {
-		return buf.Err
+		return nil
 	}
 
 	if a.Flags&AttrSize != 0 {
-		a.Size = buf.ConsumeUint64()
+		if a.Size, err = b.ConsumeUint64(); err != nil {
+			return err
+		}
 	}
 
 	if a.Flags&AttrUIDGID != 0 {
-		a.UID = buf.ConsumeUint32()
-		a.GID = buf.ConsumeUint32()
+		if a.UID, err = b.ConsumeUint32(); err != nil {
+			return err
+		}
+
+		if a.GID, err = b.ConsumeUint32(); err != nil {
+			return err
+		}
 	}
 
 	if a.Flags&AttrPermissions != 0 {
-		a.Permissions = FileMode(buf.ConsumeUint32())
+		m, err := b.ConsumeUint32()
+		if err != nil {
+			return err
+		}
+
+		a.Permissions = FileMode(m)
 	}
 
 	if a.Flags&AttrACModTime != 0 {
-		a.ATime = buf.ConsumeUint32()
-		a.MTime = buf.ConsumeUint32()
+		if a.ATime, err = b.ConsumeUint32(); err != nil {
+			return err
+		}
+
+		if a.MTime, err = b.ConsumeUint32(); err != nil {
+			return err
+		}
 	}
 
 	if a.Flags&AttrExtended != 0 {
-		count := buf.ConsumeCount()
+		count, err := b.ConsumeUint32()
+		if err != nil {
+			return err
+		}
 
 		a.ExtendedAttributes = make([]ExtendedAttribute, count)
 		for i := range a.ExtendedAttributes {
-			a.ExtendedAttributes[i].UnmarshalFrom(buf)
+			a.ExtendedAttributes[i].UnmarshalFrom(b)
 		}
 	}
 
-	return buf.Err
+	return nil
 }
 
 // UnmarshalBinary decodes the binary encoding of Attributes into e.
@@ -210,7 +233,7 @@ func (a *Attributes) UnmarshalBinary(data []byte) error {
 
 // ExtendedAttribute defines the extended file attribute type defined in draft-ietf-secsh-filexfer-02
 //
-// Defined in: https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
+// Defined in: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
 type ExtendedAttribute struct {
 	Type string
 	Data string
@@ -222,9 +245,9 @@ func (e *ExtendedAttribute) Len() int {
 }
 
 // MarshalInto marshals e onto the end of the given Buffer.
-func (e *ExtendedAttribute) MarshalInto(buf *Buffer) {
-	buf.AppendString(e.Type)
-	buf.AppendString(e.Data)
+func (e *ExtendedAttribute) MarshalInto(b *Buffer) {
+	b.AppendString(e.Type)
+	b.AppendString(e.Data)
 }
 
 // MarshalBinary returns e as the binary encoding of e.
@@ -235,13 +258,16 @@ func (e *ExtendedAttribute) MarshalBinary() ([]byte, error) {
 }
 
 // UnmarshalFrom unmarshals an ExtendedAattribute from the given Buffer into e.
-func (e *ExtendedAttribute) UnmarshalFrom(buf *Buffer) (err error) {
-	*e = ExtendedAttribute{
-		Type: buf.ConsumeString(),
-		Data: buf.ConsumeString(),
+func (e *ExtendedAttribute) UnmarshalFrom(b *Buffer) (err error) {
+	if e.Type, err = b.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	if e.Data, err = b.ConsumeString(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UnmarshalBinary decodes the binary encoding of ExtendedAttribute into e.
@@ -264,11 +290,11 @@ func (e *NameEntry) Len() int {
 }
 
 // MarshalInto marshals e onto the end of the given Buffer.
-func (e *NameEntry) MarshalInto(buf *Buffer) {
-	buf.AppendString(e.Filename)
-	buf.AppendString(e.Longname)
+func (e *NameEntry) MarshalInto(b *Buffer) {
+	b.AppendString(e.Filename)
+	b.AppendString(e.Longname)
 
-	e.Attrs.MarshalInto(buf)
+	e.Attrs.MarshalInto(b)
 }
 
 // MarshalBinary returns e as the binary encoding of e.
@@ -281,13 +307,16 @@ func (e *NameEntry) MarshalBinary() ([]byte, error) {
 // UnmarshalFrom unmarshals an NameEntry from the given Buffer into e.
 //
 // NOTE: The values of fields not covered in the a.Flags are explicitly undefined.
-func (e *NameEntry) UnmarshalFrom(buf *Buffer) (err error) {
-	*e = NameEntry{
-		Filename: buf.ConsumeString(),
-		Longname: buf.ConsumeString(),
+func (e *NameEntry) UnmarshalFrom(b *Buffer) (err error) {
+	if e.Filename, err = b.ConsumeString(); err != nil {
+		return err
+	}
+
+	if e.Longname, err = b.ConsumeString(); err != nil {
+		return err
 	}
 
-	return e.Attrs.UnmarshalFrom(buf)
+	return e.Attrs.UnmarshalFrom(b)
 }
 
 // UnmarshalBinary decodes the binary encoding of NameEntry into e.
diff --git a/internal/encoding/ssh/filexfer/buffer.go b/internal/encoding/ssh/filexfer/buffer.go
index bd4783b..a608603 100644
--- a/internal/encoding/ssh/filexfer/buffer.go
+++ b/internal/encoding/ssh/filexfer/buffer.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"encoding/binary"
@@ -17,7 +17,6 @@ var (
 type Buffer struct {
 	b   []byte
 	off int
-	Err error
 }
 
 // NewBuffer creates and initializes a new buffer using buf as its initial contents.
@@ -52,17 +51,14 @@ func (b *Buffer) Cap() int { return cap(b.b) }
 
 // Reset resets the buffer to be empty, but it retains the underlying storage for use by future Appends.
 func (b *Buffer) Reset() {
-	*b = Buffer{
-		b: b.b[:0],
-	}
+	b.b = b.b[:0]
+	b.off = 0
 }
 
 // StartPacket resets and initializes the buffer to be ready to start marshaling a packet into.
 // It truncates the buffer, reserves space for uint32(length), then appends the given packetType and requestID.
 func (b *Buffer) StartPacket(packetType PacketType, requestID uint32) {
-	*b = Buffer{
-		b: append(b.b[:0], make([]byte, 4)...),
-	}
+	b.b, b.off = append(b.b[:0], make([]byte, 4)...), 0
 
 	b.AppendUint8(uint8(packetType))
 	b.AppendUint32(requestID)
@@ -85,21 +81,15 @@ func (b *Buffer) Packet(payload []byte) (header, payloadPassThru []byte, err err
 }
 
 // ConsumeUint8 consumes a single byte from the buffer.
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeUint8() uint8 {
-	if b.Err != nil {
-		return 0
-	}
-
+// If the buffer does not have enough data, it will return ErrShortPacket.
+func (b *Buffer) ConsumeUint8() (uint8, error) {
 	if b.Len() < 1 {
-		b.off = len(b.b)
-		b.Err = ErrShortPacket
-		return 0
+		return 0, ErrShortPacket
 	}
 
 	var v uint8
 	v, b.off = b.b[b.off], b.off+1
-	return v
+	return v, nil
 }
 
 // AppendUint8 appends a single byte into the buffer.
@@ -108,9 +98,14 @@ func (b *Buffer) AppendUint8(v uint8) {
 }
 
 // ConsumeBool consumes a single byte from the buffer, and returns true if that byte is non-zero.
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeBool() bool {
-	return b.ConsumeUint8() != 0
+// If the buffer does not have enough data, it will return ErrShortPacket.
+func (b *Buffer) ConsumeBool() (bool, error) {
+	v, err := b.ConsumeUint8()
+	if err != nil {
+		return false, err
+	}
+
+	return v != 0, nil
 }
 
 // AppendBool appends a single bool into the buffer.
@@ -124,21 +119,15 @@ func (b *Buffer) AppendBool(v bool) {
 }
 
 // ConsumeUint16 consumes a single uint16 from the buffer, in network byte order (big-endian).
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeUint16() uint16 {
-	if b.Err != nil {
-		return 0
-	}
-
+// If the buffer does not have enough data, it will return ErrShortPacket.
+func (b *Buffer) ConsumeUint16() (uint16, error) {
 	if b.Len() < 2 {
-		b.off = len(b.b)
-		b.Err = ErrShortPacket
-		return 0
+		return 0, ErrShortPacket
 	}
 
 	v := binary.BigEndian.Uint16(b.b[b.off:])
 	b.off += 2
-	return v
+	return v, nil
 }
 
 // AppendUint16 appends single uint16 into the buffer, in network byte order (big-endian).
@@ -157,21 +146,15 @@ func unmarshalUint32(b []byte) uint32 {
 }
 
 // ConsumeUint32 consumes a single uint32 from the buffer, in network byte order (big-endian).
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeUint32() uint32 {
-	if b.Err != nil {
-		return 0
-	}
-
+// If the buffer does not have enough data, it will return ErrShortPacket.
+func (b *Buffer) ConsumeUint32() (uint32, error) {
 	if b.Len() < 4 {
-		b.off = len(b.b)
-		b.Err = ErrShortPacket
-		return 0
+		return 0, ErrShortPacket
 	}
 
 	v := binary.BigEndian.Uint32(b.b[b.off:])
 	b.off += 4
-	return v
+	return v, nil
 }
 
 // AppendUint32 appends a single uint32 into the buffer, in network byte order (big-endian).
@@ -184,33 +167,16 @@ func (b *Buffer) AppendUint32(v uint32) {
 	)
 }
 
-// ConsumeCount consumes a single uint32 count from the buffer, in network byte order (big-endian) as an int.
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeCount() int {
-	return int(b.ConsumeUint32())
-}
-
-// AppendCount appends a single int length as a uint32 into the buffer, in network byte order (big-endian).
-func (b *Buffer) AppendCount(v int) {
-	b.AppendUint32(uint32(v))
-}
-
 // ConsumeUint64 consumes a single uint64 from the buffer, in network byte order (big-endian).
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeUint64() uint64 {
-	if b.Err != nil {
-		return 0
-	}
-
+// If the buffer does not have enough data, it will return ErrShortPacket.
+func (b *Buffer) ConsumeUint64() (uint64, error) {
 	if b.Len() < 8 {
-		b.off = len(b.b)
-		b.Err = ErrShortPacket
-		return 0
+		return 0, ErrShortPacket
 	}
 
 	v := binary.BigEndian.Uint64(b.b[b.off:])
 	b.off += 8
-	return v
+	return v, nil
 }
 
 // AppendUint64 appends a single uint64 into the buffer, in network byte order (big-endian).
@@ -228,9 +194,14 @@ func (b *Buffer) AppendUint64(v uint64) {
 }
 
 // ConsumeInt64 consumes a single int64 from the buffer, in network byte order (big-endian) with two’s complement.
-// If the buffer does not have enough data, it will set Err to ErrShortPacket.
-func (b *Buffer) ConsumeInt64() int64 {
-	return int64(b.ConsumeUint64())
+// If the buffer does not have enough data, it will return ErrShortPacket.
+func (b *Buffer) ConsumeInt64() (int64, error) {
+	u, err := b.ConsumeUint64()
+	if err != nil {
+		return 0, err
+	}
+
+	return int64(u), err
 }
 
 // AppendInt64 appends a single int64 into the buffer, in network byte order (big-endian) with two’s complement.
@@ -240,52 +211,29 @@ func (b *Buffer) AppendInt64(v int64) {
 
 // ConsumeByteSlice consumes a single string of raw binary data from the buffer.
 // A string is a uint32 length, followed by that number of raw bytes.
-// If the buffer does not have enough data, or defines a length larger than available, it will set Err to ErrShortPacket.
+// If the buffer does not have enough data, or defines a length larger than available, it will return ErrShortPacket.
 //
 // The returned slice aliases the buffer contents, and is valid only as long as the buffer is not reused
 // (that is, only until the next call to Reset, PutLength, StartPacket, or UnmarshalBinary).
 //
 // In no case will any Consume calls return overlapping slice aliases,
 // and Append calls are guaranteed to not disturb this slice alias.
-func (b *Buffer) ConsumeByteSlice() []byte {
-	length := int(b.ConsumeUint32())
-	if b.Err != nil {
-		return nil
+func (b *Buffer) ConsumeByteSlice() ([]byte, error) {
+	length, err := b.ConsumeUint32()
+	if err != nil {
+		return nil, err
 	}
 
-	if b.Len() < length || length < 0 {
-		b.off = len(b.b)
-		b.Err = ErrShortPacket
-		return nil
+	if b.Len() < int(length) {
+		return nil, ErrShortPacket
 	}
 
 	v := b.b[b.off:]
-	if len(v) > length || cap(v) > length {
+	if len(v) > int(length) {
 		v = v[:length:length]
 	}
 	b.off += int(length)
-	return v
-}
-
-// ConsumeByteSliceCopy consumes a single string of raw binary data as a copy from the buffer.
-// A string is a uint32 length, followed by that number of raw bytes.
-// If the buffer does not have enough data, or defines a length larger than available, it will set Err to ErrShortPacket.
-//
-// The returned slice does not alias any buffer contents,
-// and will therefore be valid even if the buffer is later reused.
-//
-// If hint has sufficient capacity to hold the data, it will be reused and overwritten,
-// otherwise a new backing slice will be allocated and returned.
-func (b *Buffer) ConsumeByteSliceCopy(hint []byte) []byte {
-	data := b.ConsumeByteSlice()
-
-	if grow := len(data) - len(hint); grow > 0 {
-		hint = append(hint, make([]byte, grow)...)
-	}
-
-	n := copy(hint, data)
-	hint = hint[:n]
-	return hint
+	return v, nil
 }
 
 // AppendByteSlice appends a single string of raw binary data into the buffer.
@@ -297,12 +245,17 @@ func (b *Buffer) AppendByteSlice(v []byte) {
 
 // ConsumeString consumes a single string of binary data from the buffer.
 // A string is a uint32 length, followed by that number of raw bytes.
-// If the buffer does not have enough data, or defines a length larger than available, it will set Err to ErrShortPacket.
+// If the buffer does not have enough data, or defines a length larger than available, it will return ErrShortPacket.
 //
 // NOTE: Go implicitly assumes that strings contain UTF-8 encoded data.
 // All caveats on using arbitrary binary data in Go strings applies.
-func (b *Buffer) ConsumeString() string {
-	return string(b.ConsumeByteSlice())
+func (b *Buffer) ConsumeString() (string, error) {
+	v, err := b.ConsumeByteSlice()
+	if err != nil {
+		return "", err
+	}
+
+	return string(v), nil
 }
 
 // AppendString appends a single string of binary data into the buffer.
diff --git a/internal/encoding/ssh/filexfer/extended_packets.go b/internal/encoding/ssh/filexfer/extended_packets.go
index f717425..6b7b2ce 100644
--- a/internal/encoding/ssh/filexfer/extended_packets.go
+++ b/internal/encoding/ssh/filexfer/extended_packets.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"encoding"
@@ -86,9 +86,8 @@ func (p *ExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
 // If the extension has not been registered, then a new Buffer will be allocated.
 // Then the request-specific-data will be unmarshaled from the rest of the buffer.
 func (p *ExtendedPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	p.ExtendedRequest = buf.ConsumeString()
-	if buf.Err != nil {
-		return buf.Err
+	if p.ExtendedRequest, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
 	if p.Data == nil {
diff --git a/internal/encoding/ssh/filexfer/extensions.go b/internal/encoding/ssh/filexfer/extensions.go
index c425780..11c0b99 100644
--- a/internal/encoding/ssh/filexfer/extensions.go
+++ b/internal/encoding/ssh/filexfer/extensions.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // ExtensionPair defines the extension-pair type defined in draft-ietf-secsh-filexfer-13.
 // This type is backwards-compatible with how draft-ietf-secsh-filexfer-02 defines extensions.
@@ -29,12 +29,15 @@ func (e *ExtensionPair) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom unmarshals an ExtensionPair from the given Buffer into e.
 func (e *ExtensionPair) UnmarshalFrom(buf *Buffer) (err error) {
-	*e = ExtensionPair{
-		Name: buf.ConsumeString(),
-		Data: buf.ConsumeString(),
+	if e.Name, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	if e.Data, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UnmarshalBinary decodes the binary encoding of ExtensionPair into e.
diff --git a/internal/encoding/ssh/filexfer/filexfer.go b/internal/encoding/ssh/filexfer/filexfer.go
index d300999..1e5abf7 100644
--- a/internal/encoding/ssh/filexfer/filexfer.go
+++ b/internal/encoding/ssh/filexfer/filexfer.go
@@ -1,5 +1,5 @@
-// Package sshfx implements the wire encoding for secsh-filexfer as described in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
-package sshfx
+// Package filexfer implements the wire encoding for secsh-filexfer as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
+package filexfer
 
 // PacketMarshaller narrowly defines packets that will only be transmitted.
 //
diff --git a/internal/encoding/ssh/filexfer/fx.go b/internal/encoding/ssh/filexfer/fx.go
index 9abcbaf..48f8698 100644
--- a/internal/encoding/ssh/filexfer/fx.go
+++ b/internal/encoding/ssh/filexfer/fx.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"fmt"
@@ -10,7 +10,7 @@ type Status uint32
 // Defines the various SSH_FX_* values.
 const (
 	// see draft-ietf-secsh-filexfer-02
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-7
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7
 	StatusOK = Status(iota)
 	StatusEOF
 	StatusNoSuchFile
@@ -21,28 +21,28 @@ const (
 	StatusConnectionLost
 	StatusOPUnsupported
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-03.txt#section-7
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-7
 	StatusV4InvalidHandle
 	StatusV4NoSuchPath
 	StatusV4FileAlreadyExists
 	StatusV4WriteProtect
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-04.txt#section-7
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-7
 	StatusV4NoMedia
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-05.txt#section-7
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-7
 	StatusV5NoSpaceOnFilesystem
 	StatusV5QuotaExceeded
 	StatusV5UnknownPrincipal
 	StatusV5LockConflict
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-06.txt#section-8
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-06#section-8
 	StatusV6DirNotEmpty
 	StatusV6NotADirectory
 	StatusV6InvalidFilename
 	StatusV6LinkLoop
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-07.txt#section-8
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-8
 	StatusV6CannotDelete
 	StatusV6InvalidParameter
 	StatusV6FileIsADirectory
@@ -50,10 +50,10 @@ const (
 	StatusV6ByteRangeLockRefused
 	StatusV6DeletePending
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-08.txt#section-8.1
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-8.1
 	StatusV6FileCorrupt
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-10.txt#section-9.1
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-10#section-9.1
 	StatusV6OwnerInvalid
 	StatusV6GroupInvalid
 
diff --git a/internal/encoding/ssh/filexfer/fxp.go b/internal/encoding/ssh/filexfer/fxp.go
index 7808002..15caf6d 100644
--- a/internal/encoding/ssh/filexfer/fxp.go
+++ b/internal/encoding/ssh/filexfer/fxp.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"fmt"
@@ -9,7 +9,7 @@ type PacketType uint8
 
 // Request packet types.
 const (
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
 	PacketTypeInit = PacketType(iota + 1)
 	PacketTypeVersion
 	PacketTypeOpen
@@ -31,17 +31,17 @@ const (
 	PacketTypeReadLink
 	PacketTypeSymlink
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-07.txt#section-3.3
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-3.3
 	PacketTypeV6Link
 
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-08.txt#section-3.3
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-3.3
 	PacketTypeV6Block
 	PacketTypeV6Unblock
 )
 
 // Response packet types.
 const (
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
 	PacketTypeStatus = PacketType(iota + 101)
 	PacketTypeHandle
 	PacketTypeData
@@ -51,7 +51,7 @@ const (
 
 // Extended packet types.
 const (
-	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
+	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
 	PacketTypeExtended = PacketType(iota + 200)
 	PacketTypeExtendedReply
 )
@@ -122,48 +122,3 @@ func (f PacketType) String() string {
 		return fmt.Sprintf("SSH_FXP_UNKNOWN(%d)", f)
 	}
 }
-
-func newPacketFromType(typ PacketType) (Packet, error) {
-	switch typ {
-	case PacketTypeOpen:
-		return new(OpenPacket), nil
-	case PacketTypeClose:
-		return new(ClosePacket), nil
-	case PacketTypeRead:
-		return new(ReadPacket), nil
-	case PacketTypeWrite:
-		return new(WritePacket), nil
-	case PacketTypeLStat:
-		return new(LStatPacket), nil
-	case PacketTypeFStat:
-		return new(FStatPacket), nil
-	case PacketTypeSetstat:
-		return new(SetstatPacket), nil
-	case PacketTypeFSetstat:
-		return new(FSetstatPacket), nil
-	case PacketTypeOpenDir:
-		return new(OpenDirPacket), nil
-	case PacketTypeReadDir:
-		return new(ReadDirPacket), nil
-	case PacketTypeRemove:
-		return new(RemovePacket), nil
-	case PacketTypeMkdir:
-		return new(MkdirPacket), nil
-	case PacketTypeRmdir:
-		return new(RmdirPacket), nil
-	case PacketTypeRealPath:
-		return new(RealPathPacket), nil
-	case PacketTypeStat:
-		return new(StatPacket), nil
-	case PacketTypeRename:
-		return new(RenamePacket), nil
-	case PacketTypeReadLink:
-		return new(ReadLinkPacket), nil
-	case PacketTypeSymlink:
-		return new(SymlinkPacket), nil
-	case PacketTypeExtended:
-		return new(ExtendedPacket), nil
-	default:
-		return nil, fmt.Errorf("unexpected request packet type: %v", typ)
-	}
-}
diff --git a/internal/encoding/ssh/filexfer/handle_packets.go b/internal/encoding/ssh/filexfer/handle_packets.go
index 44594ac..a142771 100644
--- a/internal/encoding/ssh/filexfer/handle_packets.go
+++ b/internal/encoding/ssh/filexfer/handle_packets.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // ClosePacket defines the SSH_FXP_CLOSE packet.
 type ClosePacket struct {
@@ -27,18 +27,18 @@ func (p *ClosePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *ClosePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = ClosePacket{
-		Handle: buf.ConsumeString(),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // ReadPacket defines the SSH_FXP_READ packet.
 type ReadPacket struct {
 	Handle string
 	Offset uint64
-	Length uint32
+	Len    uint32
 }
 
 // Type returns the SSH_FXP_xy value associated with this packet type.
@@ -58,7 +58,7 @@ func (p *ReadPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
 	buf.StartPacket(PacketTypeRead, reqid)
 	buf.AppendString(p.Handle)
 	buf.AppendUint64(p.Offset)
-	buf.AppendUint32(p.Length)
+	buf.AppendUint32(p.Len)
 
 	return buf.Packet(payload)
 }
@@ -66,13 +66,19 @@ func (p *ReadPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *ReadPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = ReadPacket{
-		Handle: buf.ConsumeString(),
-		Offset: buf.ConsumeUint64(),
-		Length: buf.ConsumeUint32(),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	if p.Offset, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+
+	if p.Len, err = buf.ConsumeUint32(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // WritePacket defines the SSH_FXP_WRITE packet.
@@ -115,13 +121,26 @@ func (p *WritePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
 //
 // This means this _does not_ alias any of the data buffer that is passed in.
 func (p *WritePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = WritePacket{
-		Handle: buf.ConsumeString(),
-		Offset: buf.ConsumeUint64(),
-		Data:   buf.ConsumeByteSliceCopy(p.Data),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	if p.Offset, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+
+	data, err := buf.ConsumeByteSlice()
+	if err != nil {
+		return err
+	}
+
+	if len(p.Data) < len(data) {
+		p.Data = make([]byte, len(data))
 	}
 
-	return buf.Err
+	n := copy(p.Data, data)
+	p.Data = p.Data[:n]
+	return nil
 }
 
 // FStatPacket defines the SSH_FXP_FSTAT packet.
@@ -151,11 +170,11 @@ func (p *FStatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *FStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = FStatPacket{
-		Handle: buf.ConsumeString(),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // FSetstatPacket defines the SSH_FXP_FSETSTAT packet.
@@ -188,8 +207,8 @@ func (p *FSetstatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *FSetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = FSetstatPacket{
-		Handle: buf.ConsumeString(),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
 	return p.Attrs.UnmarshalFrom(buf)
@@ -222,9 +241,9 @@ func (p *ReadDirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *ReadDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = ReadDirPacket{
-		Handle: buf.ConsumeString(),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
diff --git a/internal/encoding/ssh/filexfer/init_packets.go b/internal/encoding/ssh/filexfer/init_packets.go
index c553ee2..b0bc6f5 100644
--- a/internal/encoding/ssh/filexfer/init_packets.go
+++ b/internal/encoding/ssh/filexfer/init_packets.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // InitPacket defines the SSH_FXP_INIT packet.
 type InitPacket struct {
@@ -33,8 +33,8 @@ func (p *InitPacket) MarshalBinary() ([]byte, error) {
 func (p *InitPacket) UnmarshalBinary(data []byte) (err error) {
 	buf := NewBuffer(data)
 
-	*p = InitPacket{
-		Version: buf.ConsumeUint32(),
+	if p.Version, err = buf.ConsumeUint32(); err != nil {
+		return err
 	}
 
 	for buf.Len() > 0 {
@@ -46,7 +46,7 @@ func (p *InitPacket) UnmarshalBinary(data []byte) (err error) {
 		p.Extensions = append(p.Extensions, &ext)
 	}
 
-	return buf.Err
+	return nil
 }
 
 // VersionPacket defines the SSH_FXP_VERSION packet.
@@ -82,8 +82,8 @@ func (p *VersionPacket) MarshalBinary() ([]byte, error) {
 func (p *VersionPacket) UnmarshalBinary(data []byte) (err error) {
 	buf := NewBuffer(data)
 
-	*p = VersionPacket{
-		Version: buf.ConsumeUint32(),
+	if p.Version, err = buf.ConsumeUint32(); err != nil {
+		return err
 	}
 
 	for buf.Len() > 0 {
diff --git a/internal/encoding/ssh/filexfer/open_packets.go b/internal/encoding/ssh/filexfer/open_packets.go
index 896ba16..1358711 100644
--- a/internal/encoding/ssh/filexfer/open_packets.go
+++ b/internal/encoding/ssh/filexfer/open_packets.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // SSH_FXF_* flags.
 const (
@@ -43,9 +43,12 @@ func (p *OpenPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *OpenPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = OpenPacket{
-		Filename: buf.ConsumeString(),
-		PFlags:   buf.ConsumeUint32(),
+	if p.Filename, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	if p.PFlags, err = buf.ConsumeUint32(); err != nil {
+		return err
 	}
 
 	return p.Attrs.UnmarshalFrom(buf)
@@ -78,9 +81,9 @@ func (p *OpenDirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *OpenDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = OpenDirPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
diff --git a/internal/encoding/ssh/filexfer/openssh/fsync.go b/internal/encoding/ssh/filexfer/openssh/fsync.go
index 708a4ba..7ecfb0c 100644
--- a/internal/encoding/ssh/filexfer/openssh/fsync.go
+++ b/internal/encoding/ssh/filexfer/openssh/fsync.go
@@ -60,11 +60,11 @@ func (ep *FSyncExtendedPacket) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom decodes the fsync@openssh.com extended packet-specific data from buf.
 func (ep *FSyncExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
-	*ep = FSyncExtendedPacket{
-		Handle: buf.ConsumeString(),
+	if ep.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // UnmarshalBinary decodes the fsync@openssh.com extended packet-specific data into ep.
diff --git a/internal/encoding/ssh/filexfer/openssh/hardlink.go b/internal/encoding/ssh/filexfer/openssh/hardlink.go
index f48d25a..17c3499 100644
--- a/internal/encoding/ssh/filexfer/openssh/hardlink.go
+++ b/internal/encoding/ssh/filexfer/openssh/hardlink.go
@@ -62,12 +62,15 @@ func (ep *HardlinkExtendedPacket) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom decodes the hardlink@openssh.com extended packet-specific data from buf.
 func (ep *HardlinkExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
-	*ep = HardlinkExtendedPacket{
-		OldPath: buf.ConsumeString(),
-		NewPath: buf.ConsumeString(),
+	if ep.OldPath, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	if ep.NewPath, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UnmarshalBinary decodes the hardlink@openssh.com extended packet-specific data into ep.
diff --git a/internal/encoding/ssh/filexfer/openssh/posix-rename.go b/internal/encoding/ssh/filexfer/openssh/posix-rename.go
index 5166489..a3d3de5 100644
--- a/internal/encoding/ssh/filexfer/openssh/posix-rename.go
+++ b/internal/encoding/ssh/filexfer/openssh/posix-rename.go
@@ -4,38 +4,38 @@ import (
 	sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
 )
 
-const extensionPOSIXRename = "posix-rename@openssh.com"
+const extensionPosixRename = "posix-rename@openssh.com"
 
-// RegisterExtensionPOSIXRename registers the "posix-rename@openssh.com" extended packet with the encoding/ssh/filexfer package.
-func RegisterExtensionPOSIXRename() {
-	sshfx.RegisterExtendedPacketType(extensionPOSIXRename, func() sshfx.ExtendedData {
-		return new(POSIXRenameExtendedPacket)
+// RegisterExtensionPosixRename registers the "posix-rename@openssh.com" extended packet with the encoding/ssh/filexfer package.
+func RegisterExtensionPosixRename() {
+	sshfx.RegisterExtendedPacketType(extensionPosixRename, func() sshfx.ExtendedData {
+		return new(PosixRenameExtendedPacket)
 	})
 }
 
-// ExtensionPOSIXRename returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
-func ExtensionPOSIXRename() *sshfx.ExtensionPair {
+// ExtensionPosixRename returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
+func ExtensionPosixRename() *sshfx.ExtensionPair {
 	return &sshfx.ExtensionPair{
-		Name: extensionPOSIXRename,
+		Name: extensionPosixRename,
 		Data: "1",
 	}
 }
 
-// POSIXRenameExtendedPacket defines the posix-rename@openssh.com extend packet.
-type POSIXRenameExtendedPacket struct {
+// PosixRenameExtendedPacket defines the posix-rename@openssh.com extend packet.
+type PosixRenameExtendedPacket struct {
 	OldPath string
 	NewPath string
 }
 
 // Type returns the SSH_FXP_EXTENDED packet type.
-func (ep *POSIXRenameExtendedPacket) Type() sshfx.PacketType {
+func (ep *PosixRenameExtendedPacket) Type() sshfx.PacketType {
 	return sshfx.PacketTypeExtended
 }
 
 // MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
-func (ep *POSIXRenameExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
+func (ep *PosixRenameExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
 	p := &sshfx.ExtendedPacket{
-		ExtendedRequest: extensionPOSIXRename,
+		ExtendedRequest: extensionPosixRename,
 
 		Data: ep,
 	}
@@ -43,7 +43,7 @@ func (ep *POSIXRenameExtendedPacket) MarshalPacket(reqid uint32, b []byte) (head
 }
 
 // MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
-func (ep *POSIXRenameExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
+func (ep *PosixRenameExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
 	buf.AppendString(ep.OldPath)
 	buf.AppendString(ep.NewPath)
 }
@@ -51,7 +51,7 @@ func (ep *POSIXRenameExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
 // MarshalBinary encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
 //
 // NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
-func (ep *POSIXRenameExtendedPacket) MarshalBinary() ([]byte, error) {
+func (ep *PosixRenameExtendedPacket) MarshalBinary() ([]byte, error) {
 	// string(oldpath) + string(newpath)
 	size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath)
 
@@ -61,16 +61,19 @@ func (ep *POSIXRenameExtendedPacket) MarshalBinary() ([]byte, error) {
 }
 
 // UnmarshalFrom decodes the hardlink@openssh.com extended packet-specific data from buf.
-func (ep *POSIXRenameExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
-	*ep = POSIXRenameExtendedPacket{
-		OldPath: buf.ConsumeString(),
-		NewPath: buf.ConsumeString(),
+func (ep *PosixRenameExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
+	if ep.OldPath, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	if ep.NewPath, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UnmarshalBinary decodes the hardlink@openssh.com extended packet-specific data into ep.
-func (ep *POSIXRenameExtendedPacket) UnmarshalBinary(data []byte) (err error) {
+func (ep *PosixRenameExtendedPacket) UnmarshalBinary(data []byte) (err error) {
 	return ep.UnmarshalFrom(sshfx.NewBuffer(data))
 }
diff --git a/internal/encoding/ssh/filexfer/openssh/statvfs.go b/internal/encoding/ssh/filexfer/openssh/statvfs.go
index 51029ca..3e9015f 100644
--- a/internal/encoding/ssh/filexfer/openssh/statvfs.go
+++ b/internal/encoding/ssh/filexfer/openssh/statvfs.go
@@ -61,11 +61,11 @@ func (ep *StatVFSExtendedPacket) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom decodes the statvfs@openssh.com extended packet-specific data into ep.
 func (ep *StatVFSExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
-	*ep = StatVFSExtendedPacket{
-		Path: buf.ConsumeString(),
+	if ep.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
@@ -130,11 +130,11 @@ func (ep *FStatVFSExtendedPacket) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom decodes the statvfs@openssh.com extended packet-specific data into ep.
 func (ep *FStatVFSExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
-	*ep = FStatVFSExtendedPacket{
-		Path: buf.ConsumeString(),
+	if ep.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
@@ -213,21 +213,41 @@ func (ep *StatVFSExtendedReplyPacket) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom decodes the fstatvfs@openssh.com extended reply packet-specific data into ep.
 func (ep *StatVFSExtendedReplyPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
-	*ep = StatVFSExtendedReplyPacket{
-		BlockSize:     buf.ConsumeUint64(),
-		FragmentSize:  buf.ConsumeUint64(),
-		Blocks:        buf.ConsumeUint64(),
-		BlocksFree:    buf.ConsumeUint64(),
-		BlocksAvail:   buf.ConsumeUint64(),
-		Files:         buf.ConsumeUint64(),
-		FilesFree:     buf.ConsumeUint64(),
-		FilesAvail:    buf.ConsumeUint64(),
-		FilesystemID:  buf.ConsumeUint64(),
-		MountFlags:    buf.ConsumeUint64(),
-		MaxNameLength: buf.ConsumeUint64(),
-	}
-
-	return buf.Err
+	if ep.BlockSize, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.FragmentSize, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.Blocks, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.BlocksFree, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.BlocksAvail, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.Files, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.FilesFree, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.FilesAvail, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.FilesystemID, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.MountFlags, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+	if ep.MaxNameLength, err = buf.ConsumeUint64(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UnmarshalBinary decodes the fstatvfs@openssh.com extended reply packet-specific data into ep.
diff --git a/internal/encoding/ssh/filexfer/packets.go b/internal/encoding/ssh/filexfer/packets.go
index fdf65d0..3f24e9c 100644
--- a/internal/encoding/ssh/filexfer/packets.go
+++ b/internal/encoding/ssh/filexfer/packets.go
@@ -1,13 +1,59 @@
-package sshfx
+package filexfer
 
 import (
 	"errors"
+	"fmt"
 	"io"
 )
 
 // smallBufferSize is an initial allocation minimal capacity.
 const smallBufferSize = 64
 
+func newPacketFromType(typ PacketType) (Packet, error) {
+	switch typ {
+	case PacketTypeOpen:
+		return new(OpenPacket), nil
+	case PacketTypeClose:
+		return new(ClosePacket), nil
+	case PacketTypeRead:
+		return new(ReadPacket), nil
+	case PacketTypeWrite:
+		return new(WritePacket), nil
+	case PacketTypeLStat:
+		return new(LStatPacket), nil
+	case PacketTypeFStat:
+		return new(FStatPacket), nil
+	case PacketTypeSetstat:
+		return new(SetstatPacket), nil
+	case PacketTypeFSetstat:
+		return new(FSetstatPacket), nil
+	case PacketTypeOpenDir:
+		return new(OpenDirPacket), nil
+	case PacketTypeReadDir:
+		return new(ReadDirPacket), nil
+	case PacketTypeRemove:
+		return new(RemovePacket), nil
+	case PacketTypeMkdir:
+		return new(MkdirPacket), nil
+	case PacketTypeRmdir:
+		return new(RmdirPacket), nil
+	case PacketTypeRealPath:
+		return new(RealPathPacket), nil
+	case PacketTypeStat:
+		return new(StatPacket), nil
+	case PacketTypeRename:
+		return new(RenamePacket), nil
+	case PacketTypeReadLink:
+		return new(ReadLinkPacket), nil
+	case PacketTypeSymlink:
+		return new(SymlinkPacket), nil
+	case PacketTypeExtended:
+		return new(ExtendedPacket), nil
+	default:
+		return nil, fmt.Errorf("unexpected request packet type: %v", typ)
+	}
+}
+
 // RawPacket implements the general packet format from draft-ietf-secsh-filexfer-02
 //
 // RawPacket is intended for use in clients receiving responses,
@@ -17,7 +63,7 @@ const smallBufferSize = 64
 // For servers expecting to receive arbitrary request packet types,
 // use RequestPacket.
 //
-// Defined in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
+// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
 type RawPacket struct {
 	PacketType PacketType
 	RequestID  uint32
@@ -64,14 +110,19 @@ func (p *RawPacket) MarshalBinary() ([]byte, error) {
 // The Data field will alias the passed in Buffer,
 // so the buffer passed in should not be reused before RawPacket.Reset().
 func (p *RawPacket) UnmarshalFrom(buf *Buffer) error {
-	*p = RawPacket{
-		PacketType: PacketType(buf.ConsumeUint8()),
-		RequestID:  buf.ConsumeUint32(),
+	typ, err := buf.ConsumeUint8()
+	if err != nil {
+		return err
 	}
 
-	p.Data = *buf
+	p.PacketType = PacketType(typ)
 
-	return buf.Err
+	if p.RequestID, err = buf.ConsumeUint32(); err != nil {
+		return err
+	}
+
+	p.Data = *buf
+	return nil
 }
 
 // UnmarshalBinary decodes a full raw packet out of the given data.
@@ -174,7 +225,7 @@ func (p *RawPacket) ReadFrom(r io.Reader, b []byte, maxPacketLength uint32) erro
 // where automatic unmarshaling of the packet body does not make sense,
 // use RawPacket.
 //
-// Defined in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
+// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
 type RequestPacket struct {
 	RequestID uint32
 
@@ -217,19 +268,18 @@ func (p *RequestPacket) MarshalBinary() ([]byte, error) {
 // The Request field may alias the passed in Buffer, (e.g. SSH_FXP_WRITE),
 // so the buffer passed in should not be reused before RequestPacket.Reset().
 func (p *RequestPacket) UnmarshalFrom(buf *Buffer) error {
-	typ := PacketType(buf.ConsumeUint8())
-	if buf.Err != nil {
-		return buf.Err
+	typ, err := buf.ConsumeUint8()
+	if err != nil {
+		return err
 	}
 
-	req, err := newPacketFromType(typ)
+	p.Request, err = newPacketFromType(PacketType(typ))
 	if err != nil {
 		return err
 	}
 
-	*p = RequestPacket{
-		RequestID: buf.ConsumeUint32(),
-		Request:   req,
+	if p.RequestID, err = buf.ConsumeUint32(); err != nil {
+		return err
 	}
 
 	return p.Request.UnmarshalPacketBody(buf)
diff --git a/internal/encoding/ssh/filexfer/path_packets.go b/internal/encoding/ssh/filexfer/path_packets.go
index 0180326..5792d47 100644
--- a/internal/encoding/ssh/filexfer/path_packets.go
+++ b/internal/encoding/ssh/filexfer/path_packets.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // LStatPacket defines the SSH_FXP_LSTAT packet.
 type LStatPacket struct {
@@ -27,11 +27,11 @@ func (p *LStatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *LStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = LStatPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // SetstatPacket defines the SSH_FXP_SETSTAT packet.
@@ -64,8 +64,8 @@ func (p *SetstatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *SetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = SetstatPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
 	return p.Attrs.UnmarshalFrom(buf)
@@ -98,11 +98,11 @@ func (p *RemovePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *RemovePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = RemovePacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // MkdirPacket defines the SSH_FXP_MKDIR packet.
@@ -135,8 +135,8 @@ func (p *MkdirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *MkdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = MkdirPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
 	return p.Attrs.UnmarshalFrom(buf)
@@ -169,11 +169,11 @@ func (p *RmdirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *RmdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = RmdirPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // RealPathPacket defines the SSH_FXP_REALPATH packet.
@@ -203,11 +203,11 @@ func (p *RealPathPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *RealPathPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = RealPathPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // StatPacket defines the SSH_FXP_STAT packet.
@@ -237,11 +237,11 @@ func (p *StatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *StatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = StatPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // RenamePacket defines the SSH_FXP_RENAME packet.
@@ -274,12 +274,15 @@ func (p *RenamePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *RenamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = RenamePacket{
-		OldPath: buf.ConsumeString(),
-		NewPath: buf.ConsumeString(),
+	if p.OldPath, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	if p.NewPath, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // ReadLinkPacket defines the SSH_FXP_READLINK packet.
@@ -309,11 +312,11 @@ func (p *ReadLinkPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *ReadLinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = ReadLinkPacket{
-		Path: buf.ConsumeString(),
+	if p.Path, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // SymlinkPacket defines the SSH_FXP_SYMLINK packet.
@@ -352,11 +355,14 @@ func (p *SymlinkPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *SymlinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = SymlinkPacket{
-		// Arguments were inadvertently reversed.
-		TargetPath: buf.ConsumeString(),
-		LinkPath:   buf.ConsumeString(),
+	// Arguments were inadvertently reversed.
+	if p.TargetPath, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	if p.LinkPath, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
diff --git a/internal/encoding/ssh/filexfer/permissions.go b/internal/encoding/ssh/filexfer/permissions.go
index 0143ec0..2fe63d5 100644
--- a/internal/encoding/ssh/filexfer/permissions.go
+++ b/internal/encoding/ssh/filexfer/permissions.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 // FileMode represents a file’s mode and permission bits.
 // The bits are defined according to POSIX standards,
diff --git a/internal/encoding/ssh/filexfer/response_packets.go b/internal/encoding/ssh/filexfer/response_packets.go
index 311708f..7a9b3ea 100644
--- a/internal/encoding/ssh/filexfer/response_packets.go
+++ b/internal/encoding/ssh/filexfer/response_packets.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"fmt"
@@ -6,7 +6,7 @@ import (
 
 // StatusPacket defines the SSH_FXP_STATUS packet.
 //
-// Specified in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-7
+// Specified in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7
 type StatusPacket struct {
 	StatusCode   Status
 	ErrorMessage string
@@ -19,7 +19,7 @@ func (p *StatusPacket) Error() string {
 		return "sftp: " + p.StatusCode.String()
 	}
 
-	return fmt.Sprintf("sftp: %s: %q", p.StatusCode, p.ErrorMessage)
+	return fmt.Sprintf("sftp: %q (%s)", p.ErrorMessage, p.StatusCode)
 }
 
 // Is returns true if target is a StatusPacket with the same StatusCode,
@@ -57,13 +57,21 @@ func (p *StatusPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *StatusPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = StatusPacket{
-		StatusCode:   Status(buf.ConsumeUint32()),
-		ErrorMessage: buf.ConsumeString(),
-		LanguageTag:  buf.ConsumeString(),
+	statusCode, err := buf.ConsumeUint32()
+	if err != nil {
+		return err
 	}
+	p.StatusCode = Status(statusCode)
 
-	return buf.Err
+	if p.ErrorMessage, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	if p.LanguageTag, err = buf.ConsumeString(); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // HandlePacket defines the SSH_FXP_HANDLE packet.
@@ -93,11 +101,11 @@ func (p *HandlePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *HandlePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = HandlePacket{
-		Handle: buf.ConsumeString(),
+	if p.Handle, err = buf.ConsumeString(); err != nil {
+		return err
 	}
 
-	return buf.Err
+	return nil
 }
 
 // DataPacket defines the SSH_FXP_DATA packet.
@@ -135,11 +143,18 @@ func (p *DataPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
 //
 // This means this _does not_ alias any of the data buffer that is passed in.
 func (p *DataPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	*p = DataPacket{
-		Data: buf.ConsumeByteSliceCopy(p.Data),
+	data, err := buf.ConsumeByteSlice()
+	if err != nil {
+		return err
 	}
 
-	return buf.Err
+	if len(p.Data) < len(data) {
+		p.Data = make([]byte, len(data))
+	}
+
+	n := copy(p.Data, data)
+	p.Data = p.Data[:n]
+	return nil
 }
 
 // NamePacket defines the SSH_FXP_NAME packet.
@@ -178,16 +193,14 @@ func (p *NamePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
 // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
 // It is assumed that the uint32(request-id) has already been consumed.
 func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
-	count := buf.ConsumeCount()
-	if buf.Err != nil {
-		return buf.Err
+	count, err := buf.ConsumeUint32()
+	if err != nil {
+		return err
 	}
 
-	*p = NamePacket{
-		Entries: make([]*NameEntry, 0, count),
-	}
+	p.Entries = make([]*NameEntry, 0, count)
 
-	for i := 0; i < count; i++ {
+	for i := uint32(0); i < count; i++ {
 		var e NameEntry
 		if err := e.UnmarshalFrom(buf); err != nil {
 			return err
@@ -196,7 +209,7 @@ func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
 		p.Entries = append(p.Entries, &e)
 	}
 
-	return buf.Err
+	return nil
 }
 
 // AttrsPacket defines the SSH_FXP_ATTRS packet.
diff --git a/ls_formatting.go b/ls_formatting.go
index e083e22..19271ad 100644
--- a/ls_formatting.go
+++ b/ls_formatting.go
@@ -60,6 +60,13 @@ func runLs(idLookup NameLookupFileLister, dirent os.FileInfo) string {
 		uid = lsFormatID(sys.UID)
 		gid = lsFormatID(sys.GID)
 	default:
+		if fiExt, ok := dirent.(FileInfoUidGid); ok {
+			uid = lsFormatID(fiExt.Uid())
+			gid = lsFormatID(fiExt.Gid())
+
+			break
+		}
+
 		numLinks, uid, gid = lsLinksUIDGID(dirent)
 	}
 
diff --git a/ls_plan9.go b/ls_plan9.go
index b70b294..a16a3ea 100644
--- a/ls_plan9.go
+++ b/ls_plan9.go
@@ -1,4 +1,3 @@
-//go:build plan9
 // +build plan9
 
 package sftp
diff --git a/ls_stub.go b/ls_stub.go
index f58abf7..6dec393 100644
--- a/ls_stub.go
+++ b/ls_stub.go
@@ -1,4 +1,3 @@
-//go:build windows || android
 // +build windows android
 
 package sftp
diff --git a/ls_unix.go b/ls_unix.go
index 0beba32..59ccffd 100644
--- a/ls_unix.go
+++ b/ls_unix.go
@@ -1,4 +1,3 @@
-//go:build aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || js
 // +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js
 
 package sftp
diff --git a/packet-manager.go b/packet-manager.go
index 647836b..c740c4c 100644
--- a/packet-manager.go
+++ b/packet-manager.go
@@ -40,7 +40,7 @@ func newPktMgr(sender packetSender) *packetManager {
 	return s
 }
 
-// // packet ordering
+//// packet ordering
 func (s *packetManager) newOrderID() uint32 {
 	s.packetCount++
 	return s.packetCount
@@ -89,7 +89,7 @@ func (o orderedPackets) Sort() {
 	})
 }
 
-// // packet registry
+//// packet registry
 // register incoming packets to be handled
 func (s *packetManager) incomingPacket(pkt orderedRequest) {
 	s.working.Add(1)
diff --git a/packet-typing.go b/packet-typing.go
index fec88e7..f4f9052 100644
--- a/packet-typing.go
+++ b/packet-typing.go
@@ -31,7 +31,7 @@ type notReadOnly interface {
 	notReadOnly()
 }
 
-// // define types by adding methods
+//// define types by adding methods
 // hasPath
 func (p *sshFxpLstatPacket) getPath() string    { return p.Path }
 func (p *sshFxpStatPacket) getPath() string     { return p.Path }
diff --git a/packet.go b/packet.go
index d89ad99..b06d0b9 100644
--- a/packet.go
+++ b/packet.go
@@ -71,6 +71,15 @@ func marshalFileInfo(b []byte, fi os.FileInfo) []byte {
 		b = marshalUint32(b, fileStat.Mtime)
 	}
 
+	if flags&sshFileXferAttrExtended != 0 {
+		b = marshalUint32(b, uint32(len(fileStat.Extended)))
+
+		for _, attr := range fileStat.Extended {
+			b = marshalString(b, attr.ExtType)
+			b = marshalString(b, attr.ExtData)
+		}
+	}
+
 	return b
 }
 
diff --git a/release.go b/release.go
index 9ecedc4..b695528 100644
--- a/release.go
+++ b/release.go
@@ -1,4 +1,3 @@
-//go:build !debug
 // +build !debug
 
 package sftp
diff --git a/request-interfaces.go b/request-interfaces.go
index 75c6c39..2090e31 100644
--- a/request-interfaces.go
+++ b/request-interfaces.go
@@ -75,7 +75,7 @@ type StatVFSFileCmder interface {
 // Note in cases of an error, the error text will be sent to the client.
 // Called for Methods: List, Stat, Readlink
 //
-// Since Filelist returns an os.FileInfo, this can make it non-ideal for impelmenting Readlink.
+// Since Filelist returns an os.FileInfo, this can make it non-ideal for implementing Readlink.
 // This is because the Name receiver method defined by that interface defines that it should only return the base name.
 // However, Readlink is required to be capable of returning essentially any arbitrary valid path relative or absolute.
 // In order to implement this more expressive requirement, implement [ReadlinkFileLister] which will then be used instead.
@@ -131,11 +131,19 @@ type NameLookupFileLister interface {
 	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
-// copied to how many could be copied (eg. n < len(ls) below).
+// ListerAt does for file lists what io.ReaderAt does for files, i.e. a []os.FileInfo buffer is passed to the ListAt function
+// and the entries that are populated in the buffer will be passed to the client.
+//
+// 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 copied to how many could be copied (eg. n < len(ls) below).
 // The copy() builtin is best for the copying.
+//
+// Uid and gid information will on unix systems be retrieved from [os.FileInfo.Sys]
+// if this function returns a [syscall.Stat_t] when called on a populated entry.
+// Alternatively, if the entry implements [FileInfoUidGid], it will be used for uid and gid information.
+//
+// If a populated entry implements [FileInfoExtendedData], extended attributes will also be returned to the client.
+//
 // Note in cases of an error, the error text will be sent to the client.
 type ListerAt interface {
 	ListAt([]os.FileInfo, int64) (int, error)
diff --git a/request-plan9.go b/request-plan9.go
index 38f91bc..0e3b483 100644
--- a/request-plan9.go
+++ b/request-plan9.go
@@ -1,4 +1,3 @@
-//go:build plan9
 // +build plan9
 
 package sftp
diff --git a/request-readme.md b/request-readme.md
index f8b81f3..f887274 100644
--- a/request-readme.md
+++ b/request-readme.md
@@ -28,7 +28,7 @@ then sends to the client.
 Handler for "Put" method and returns an io.Writer for the file which the server
 then writes the uploaded file to. The file opening "pflags" are currently
 preserved in the Request.Flags field as a 32bit bitmask value. See the [SFTP
-spec](https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-6.3) for
+spec](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.3) for
 details.
 
 ###    Filecmd(*Request) error
diff --git a/request-unix.go b/request-unix.go
index e3e037d..d30b256 100644
--- a/request-unix.go
+++ b/request-unix.go
@@ -1,4 +1,3 @@
-//go:build !windows && !plan9
 // +build !windows,!plan9
 
 package sftp
diff --git a/server.go b/server.go
index 503454e..a09741c 100644
--- a/server.go
+++ b/server.go
@@ -24,7 +24,7 @@ const (
 // Server is an SSH File Transfer Protocol (sftp) server.
 // This is intended to provide the sftp subsystem to an ssh server daemon.
 // This implementation currently supports most of sftp server protocol version 3,
-// as specified at https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
+// as specified at http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
 type Server struct {
 	*serverConn
 	debugStream   io.Writer
diff --git a/server_statvfs_impl.go b/server_statvfs_impl.go
index a547079..94b6d83 100644
--- a/server_statvfs_impl.go
+++ b/server_statvfs_impl.go
@@ -1,4 +1,3 @@
-//go:build darwin || linux
 // +build darwin linux
 
 // fill in statvfs structure with OS specific values
diff --git a/server_statvfs_linux.go b/server_statvfs_linux.go
index 615c415..1d180d4 100644
--- a/server_statvfs_linux.go
+++ b/server_statvfs_linux.go
@@ -1,4 +1,3 @@
-//go:build linux
 // +build linux
 
 package sftp
diff --git a/server_statvfs_stubs.go b/server_statvfs_stubs.go
index dd4705b..fbf4906 100644
--- a/server_statvfs_stubs.go
+++ b/server_statvfs_stubs.go
@@ -1,4 +1,3 @@
-//go:build !darwin && !linux && !plan9
 // +build !darwin,!linux,!plan9
 
 package sftp
diff --git a/sftp.go b/sftp.go
index 778c8f3..9a63c39 100644
--- a/sftp.go
+++ b/sftp.go
@@ -1,5 +1,5 @@
 // Package sftp implements the SSH File Transfer Protocol as described in
-// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
+// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
 package sftp
 
 import (
diff --git a/syscall_fixed.go b/syscall_fixed.go
index e844308..d404577 100644
--- a/syscall_fixed.go
+++ b/syscall_fixed.go
@@ -1,4 +1,3 @@
-//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
diff --git a/syscall_good.go b/syscall_good.go
index 5005218..4c2b240 100644
--- a/syscall_good.go
+++ b/syscall_good.go
@@ -1,6 +1,4 @@
-//go:build !plan9 && !windows && (!js || !wasm)
-// +build !plan9
-// +build !windows
+// +build !plan9,!windows
 // +build !js !wasm
 
 package sftp


Test patch:
diff --git a/client_integration_test.go b/client_integration_test.go
index 35ccbea..c57ae77 100644
--- a/client_integration_test.go
+++ b/client_integration_test.go
@@ -651,69 +651,6 @@ func TestClientRemove(t *testing.T) {
 	}
 }
 
-func TestClientRemoveAll(t *testing.T) {
-	sftp, cmd := testClient(t, READWRITE, NODELAY)
-	defer cmd.Wait()
-	defer sftp.Close()
-
-	// Create a temporary directory for testing
-	tempDir, err := ioutil.TempDir("", "sftptest-removeAll")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tempDir)
-
-	// Create a directory tree
-	dir1, err := ioutil.TempDir(tempDir, "foo")
-	if err != nil {
-		t.Fatal(err)
-	}
-	dir2, err := ioutil.TempDir(dir1, "bar")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Create some files within the directory tree
-	file1 := tempDir + "/file1.txt"
-	file2 := dir1 + "/file2.txt"
-	file3 := dir2 + "/file3.txt"
-	err = ioutil.WriteFile(file1, []byte("File 1"), 0644)
-	if err != nil {
-		t.Fatalf("Failed to create file: %v", err)
-	}
-	err = ioutil.WriteFile(file2, []byte("File 2"), 0644)
-	if err != nil {
-		t.Fatalf("Failed to create file: %v", err)
-	}
-	err = ioutil.WriteFile(file3, []byte("File 3"), 0644)
-	if err != nil {
-		t.Fatalf("Failed to create file: %v", err)
-	}
-
-	// Call the function to delete the files recursively
-	err = sftp.RemoveAll(tempDir)
-	if err != nil {
-		t.Fatalf("Failed to delete files recursively: %v", err)
-	}
-
-	// Check if the directories and files have been deleted
-	if _, err := os.Stat(dir1); !os.IsNotExist(err) {
-		t.Errorf("Directory %s still exists", dir1)
-	}
-	if _, err := os.Stat(dir2); !os.IsNotExist(err) {
-		t.Errorf("Directory %s still exists", dir2)
-	}
-	if _, err := os.Stat(file1); !os.IsNotExist(err) {
-		t.Errorf("File %s still exists", file1)
-	}
-	if _, err := os.Stat(file2); !os.IsNotExist(err) {
-		t.Errorf("File %s still exists", file2)
-	}
-	if _, err := os.Stat(file3); !os.IsNotExist(err) {
-		t.Errorf("File %s still exists", file3)
-	}
-}
-
 func TestClientRemoveDir(t *testing.T) {
 	sftp, cmd := testClient(t, READWRITE, NODELAY)
 	defer cmd.Wait()
diff --git a/internal/encoding/ssh/filexfer/attrs_test.go b/internal/encoding/ssh/filexfer/attrs_test.go
index 1e620fa..6716a57 100644
--- a/internal/encoding/ssh/filexfer/attrs_test.go
+++ b/internal/encoding/ssh/filexfer/attrs_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/extended_packets_test.go b/internal/encoding/ssh/filexfer/extended_packets_test.go
index d7a54d3..0860773 100644
--- a/internal/encoding/ssh/filexfer/extended_packets_test.go
+++ b/internal/encoding/ssh/filexfer/extended_packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
@@ -20,9 +20,9 @@ func (d *testExtendedData) MarshalBinary() ([]byte, error) {
 func (d *testExtendedData) UnmarshalBinary(data []byte) error {
 	buf := NewBuffer(data)
 
-	v := buf.ConsumeUint8()
-	if buf.Err != nil {
-		return buf.Err
+	v, err := buf.ConsumeUint8()
+	if err != nil {
+		return err
 	}
 
 	d.value = v ^ 0x2a
diff --git a/internal/encoding/ssh/filexfer/extensions_test.go b/internal/encoding/ssh/filexfer/extensions_test.go
index 15f2a79..453265b 100644
--- a/internal/encoding/ssh/filexfer/extensions_test.go
+++ b/internal/encoding/ssh/filexfer/extensions_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/fx_test.go b/internal/encoding/ssh/filexfer/fx_test.go
index 9548059..3e8db1d 100644
--- a/internal/encoding/ssh/filexfer/fx_test.go
+++ b/internal/encoding/ssh/filexfer/fx_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bufio"
@@ -9,7 +9,7 @@ import (
 	"testing"
 )
 
-// This string data is copied verbatim from https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-13.txt
+// This string data is copied verbatim from https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
 var fxStandardsText = `
 SSH_FX_OK                            0
 SSH_FX_EOF                           1
diff --git a/internal/encoding/ssh/filexfer/fxp_test.go b/internal/encoding/ssh/filexfer/fxp_test.go
index b0a3a83..14e8ff7 100644
--- a/internal/encoding/ssh/filexfer/fxp_test.go
+++ b/internal/encoding/ssh/filexfer/fxp_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bufio"
@@ -8,8 +8,7 @@ import (
 	"testing"
 )
 
-// This string data is copied verbatim from https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-13.txt
-// except where commented that it was taken from a different source.
+// This string data is copied verbatim from https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
 var fxpStandardsText = `
 SSH_FXP_INIT                1
 SSH_FXP_VERSION             2
diff --git a/internal/encoding/ssh/filexfer/handle_packets_test.go b/internal/encoding/ssh/filexfer/handle_packets_test.go
index 249ff32..bd034a1 100644
--- a/internal/encoding/ssh/filexfer/handle_packets_test.go
+++ b/internal/encoding/ssh/filexfer/handle_packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
@@ -58,7 +58,7 @@ func TestReadPacket(t *testing.T) {
 	p := &ReadPacket{
 		Handle: "somehandle",
 		Offset: offset,
-		Length: length,
+		Len:    length,
 	}
 
 	buf, err := ComposePacket(p.MarshalPacket(id, nil))
@@ -94,8 +94,8 @@ func TestReadPacket(t *testing.T) {
 		t.Errorf("UnmarshalPacketBody(): Offset was %x, but expected %x", p.Offset, offset)
 	}
 
-	if p.Length != length {
-		t.Errorf("UnmarshalPacketBody(): Length was %x, but expected %x", p.Length, length)
+	if p.Len != length {
+		t.Errorf("UnmarshalPacketBody(): Len was %x, but expected %x", p.Len, length)
 	}
 }
 
diff --git a/internal/encoding/ssh/filexfer/init_packets_test.go b/internal/encoding/ssh/filexfer/init_packets_test.go
index 0c466bf..e7605f9 100644
--- a/internal/encoding/ssh/filexfer/init_packets_test.go
+++ b/internal/encoding/ssh/filexfer/init_packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/open_packets_test.go b/internal/encoding/ssh/filexfer/open_packets_test.go
index a792bf4..a6b4ac0 100644
--- a/internal/encoding/ssh/filexfer/open_packets_test.go
+++ b/internal/encoding/ssh/filexfer/open_packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go b/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
index 58e4fff..6bdb10d 100644
--- a/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
+++ b/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
@@ -7,20 +7,20 @@ import (
 	sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
 )
 
-var _ sshfx.PacketMarshaller = &POSIXRenameExtendedPacket{}
+var _ sshfx.PacketMarshaller = &PosixRenameExtendedPacket{}
 
 func init() {
-	RegisterExtensionPOSIXRename()
+	RegisterExtensionPosixRename()
 }
 
-func TestPOSIXRenameExtendedPacket(t *testing.T) {
+func TestPosixRenameExtendedPacket(t *testing.T) {
 	const (
 		id      = 42
 		oldpath = "/foo"
 		newpath = "/bar"
 	)
 
-	ep := &POSIXRenameExtendedPacket{
+	ep := &PosixRenameExtendedPacket{
 		OldPath: oldpath,
 		NewPath: newpath,
 	}
@@ -50,13 +50,13 @@ func TestPOSIXRenameExtendedPacket(t *testing.T) {
 		t.Fatal("unexpected error:", err)
 	}
 
-	if p.ExtendedRequest != extensionPOSIXRename {
-		t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionPOSIXRename)
+	if p.ExtendedRequest != extensionPosixRename {
+		t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionPosixRename)
 	}
 
-	ep, ok := p.Data.(*POSIXRenameExtendedPacket)
+	ep, ok := p.Data.(*PosixRenameExtendedPacket)
 	if !ok {
-		t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *POSIXRenameExtendedPacket", p.Data)
+		t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *PosixRenameExtendedPacket", p.Data)
 	}
 
 	if ep.OldPath != oldpath {
diff --git a/internal/encoding/ssh/filexfer/packets_test.go b/internal/encoding/ssh/filexfer/packets_test.go
index f6dac8e..1600920 100644
--- a/internal/encoding/ssh/filexfer/packets_test.go
+++ b/internal/encoding/ssh/filexfer/packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/path_packets_test.go b/internal/encoding/ssh/filexfer/path_packets_test.go
index f016f7e..bad8528 100644
--- a/internal/encoding/ssh/filexfer/path_packets_test.go
+++ b/internal/encoding/ssh/filexfer/path_packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/response_packets_test.go b/internal/encoding/ssh/filexfer/response_packets_test.go
index 3f8c5b0..fd00601 100644
--- a/internal/encoding/ssh/filexfer/response_packets_test.go
+++ b/internal/encoding/ssh/filexfer/response_packets_test.go
@@ -1,4 +1,4 @@
-package sshfx
+package filexfer
 
 import (
 	"bytes"
diff --git a/ls_formatting_test.go b/ls_formatting_test.go
index 8d0fefb..c9c63a1 100644
--- a/ls_formatting_test.go
+++ b/ls_formatting_test.go
@@ -42,41 +42,41 @@ func TestRunLsWithLicensesFileWithOSLookup(t *testing.T) {
 }
 
 /*
-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.
+   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