Repository: pkg/sftp
Pull request #545 (2023-03-27 17:09:08)

refactor sshfx encoding, fix link rot, go fmt
I’ve poked around with the wire formatting code, and switched from a “check error every time” to “accumulate an error” which allows for more direct and concise unmarshals in general.

Also, I found the ietf draft link rot #544 and updated each of the ones that points to a version earlier than `-13`. (The `-13` links are still valid.)

Also, we’ve accumulated some `go fmt` changes that show up in go 1.20, so I threw those in as well.


Patch:
diff --git a/attrs.go b/attrs.go
index 2bb2d57..cb0987d 100644
--- a/attrs.go
+++ b/attrs.go
@@ -1,7 +1,7 @@
 package sftp
 
 // ssh_FXP_ATTRS support
-// see http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
+// see https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
 
 import (
 	"os"
diff --git a/attrs_stubs.go b/attrs_stubs.go
index c01f336..d20348c 100644
--- a/attrs_stubs.go
+++ b/attrs_stubs.go
@@ -1,3 +1,4 @@
+//go:build plan9 || windows || android
 // +build plan9 windows android
 
 package sftp
diff --git a/attrs_unix.go b/attrs_unix.go
index d1f4452..371ae9b 100644
--- a/attrs_unix.go
+++ b/attrs_unix.go
@@ -1,3 +1,4 @@
+//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 975bb7a..fb0a110 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 // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
+const sftpProtocolVersion = 3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
 
 func (c *Client) sendInit() error {
 	return c.clientConn.conn.sendPacket(&sshFxInitPacket{
-		Version: sftpProtocolVersion, // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
+		Version: sftpProtocolVersion, // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
 	})
 }
 
diff --git a/debug.go b/debug.go
index 3e264ab..f0db14d 100644
--- a/debug.go
+++ b/debug.go
@@ -1,3 +1,4 @@
+//go:build debug
 // +build debug
 
 package sftp
diff --git a/fuzz.go b/fuzz.go
index 169aebc..f2f1fc3 100644
--- a/fuzz.go
+++ b/fuzz.go
@@ -1,3 +1,4 @@
+//go:build gofuzz
 // +build gofuzz
 
 package sftp
diff --git a/internal/encoding/ssh/filexfer/attrs.go b/internal/encoding/ssh/filexfer/attrs.go
index eed61bf..3aec937 100644
--- a/internal/encoding/ssh/filexfer/attrs.go
+++ b/internal/encoding/ssh/filexfer/attrs.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // 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://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
+// Defined in: https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#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(b *Buffer) {
-	b.AppendUint32(a.Flags)
+func (a *Attributes) MarshalInto(buf *Buffer) {
+	buf.AppendUint32(a.Flags)
 
 	if a.Flags&AttrSize != 0 {
-		b.AppendUint64(a.Size)
+		buf.AppendUint64(a.Size)
 	}
 
 	if a.Flags&AttrUIDGID != 0 {
-		b.AppendUint32(a.UID)
-		b.AppendUint32(a.GID)
+		buf.AppendUint32(a.UID)
+		buf.AppendUint32(a.GID)
 	}
 
 	if a.Flags&AttrPermissions != 0 {
-		b.AppendUint32(uint32(a.Permissions))
+		buf.AppendUint32(uint32(a.Permissions))
 	}
 
 	if a.Flags&AttrACModTime != 0 {
-		b.AppendUint32(a.ATime)
-		b.AppendUint32(a.MTime)
+		buf.AppendUint32(a.ATime)
+		buf.AppendUint32(a.MTime)
 	}
 
 	if a.Flags&AttrExtended != 0 {
-		b.AppendUint32(uint32(len(a.ExtendedAttributes)))
+		buf.AppendUint32(uint32(len(a.ExtendedAttributes)))
 
 		for _, ext := range a.ExtendedAttributes {
-			ext.MarshalInto(b)
+			ext.MarshalInto(buf)
 		}
 	}
 }
@@ -156,74 +156,51 @@ 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(b *Buffer) (err error) {
-	flags, err := b.ConsumeUint32()
-	if err != nil {
-		return err
-	}
+func (a *Attributes) UnmarshalFrom(buf *Buffer) (err error) {
+	flags := buf.ConsumeUint32()
 
-	return a.XXX_UnmarshalByFlags(flags, b)
+	return a.XXX_UnmarshalByFlags(flags, buf)
 }
 
 // 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, b *Buffer) (err error) {
+func (a *Attributes) XXX_UnmarshalByFlags(flags uint32, buf *Buffer) (err error) {
 	a.Flags = flags
 
 	// Short-circuit dummy attributes.
 	if a.Flags == 0 {
-		return nil
+		return buf.Err
 	}
 
 	if a.Flags&AttrSize != 0 {
-		if a.Size, err = b.ConsumeUint64(); err != nil {
-			return err
-		}
+		a.Size = buf.ConsumeUint64()
 	}
 
 	if a.Flags&AttrUIDGID != 0 {
-		if a.UID, err = b.ConsumeUint32(); err != nil {
-			return err
-		}
-
-		if a.GID, err = b.ConsumeUint32(); err != nil {
-			return err
-		}
+		a.UID = buf.ConsumeUint32()
+		a.GID = buf.ConsumeUint32()
 	}
 
 	if a.Flags&AttrPermissions != 0 {
-		m, err := b.ConsumeUint32()
-		if err != nil {
-			return err
-		}
-
-		a.Permissions = FileMode(m)
+		a.Permissions = FileMode(buf.ConsumeUint32())
 	}
 
 	if a.Flags&AttrACModTime != 0 {
-		if a.ATime, err = b.ConsumeUint32(); err != nil {
-			return err
-		}
-
-		if a.MTime, err = b.ConsumeUint32(); err != nil {
-			return err
-		}
+		a.ATime = buf.ConsumeUint32()
+		a.MTime = buf.ConsumeUint32()
 	}
 
 	if a.Flags&AttrExtended != 0 {
-		count, err := b.ConsumeUint32()
-		if err != nil {
-			return err
-		}
+		count := buf.ConsumeCount()
 
 		a.ExtendedAttributes = make([]ExtendedAttribute, count)
 		for i := range a.ExtendedAttributes {
-			a.ExtendedAttributes[i].UnmarshalFrom(b)
+			a.ExtendedAttributes[i].UnmarshalFrom(buf)
 		}
 	}
 
-	return nil
+	return buf.Err
 }
 
 // UnmarshalBinary decodes the binary encoding of Attributes into e.
@@ -233,7 +210,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://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
+// Defined in: https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
 type ExtendedAttribute struct {
 	Type string
 	Data string
@@ -245,9 +222,9 @@ func (e *ExtendedAttribute) Len() int {
 }
 
 // MarshalInto marshals e onto the end of the given Buffer.
-func (e *ExtendedAttribute) MarshalInto(b *Buffer) {
-	b.AppendString(e.Type)
-	b.AppendString(e.Data)
+func (e *ExtendedAttribute) MarshalInto(buf *Buffer) {
+	buf.AppendString(e.Type)
+	buf.AppendString(e.Data)
 }
 
 // MarshalBinary returns e as the binary encoding of e.
@@ -258,16 +235,13 @@ func (e *ExtendedAttribute) MarshalBinary() ([]byte, error) {
 }
 
 // UnmarshalFrom unmarshals an ExtendedAattribute from the given Buffer into e.
-func (e *ExtendedAttribute) UnmarshalFrom(b *Buffer) (err error) {
-	if e.Type, err = b.ConsumeString(); err != nil {
-		return err
+func (e *ExtendedAttribute) UnmarshalFrom(buf *Buffer) (err error) {
+	*e = ExtendedAttribute{
+		Type: buf.ConsumeString(),
+		Data: buf.ConsumeString(),
 	}
 
-	if e.Data, err = b.ConsumeString(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // UnmarshalBinary decodes the binary encoding of ExtendedAttribute into e.
@@ -290,11 +264,11 @@ func (e *NameEntry) Len() int {
 }
 
 // MarshalInto marshals e onto the end of the given Buffer.
-func (e *NameEntry) MarshalInto(b *Buffer) {
-	b.AppendString(e.Filename)
-	b.AppendString(e.Longname)
+func (e *NameEntry) MarshalInto(buf *Buffer) {
+	buf.AppendString(e.Filename)
+	buf.AppendString(e.Longname)
 
-	e.Attrs.MarshalInto(b)
+	e.Attrs.MarshalInto(buf)
 }
 
 // MarshalBinary returns e as the binary encoding of e.
@@ -307,16 +281,13 @@ 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(b *Buffer) (err error) {
-	if e.Filename, err = b.ConsumeString(); err != nil {
-		return err
-	}
-
-	if e.Longname, err = b.ConsumeString(); err != nil {
-		return err
+func (e *NameEntry) UnmarshalFrom(buf *Buffer) (err error) {
+	*e = NameEntry{
+		Filename: buf.ConsumeString(),
+		Longname: buf.ConsumeString(),
 	}
 
-	return e.Attrs.UnmarshalFrom(b)
+	return e.Attrs.UnmarshalFrom(buf)
 }
 
 // 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 a608603..bd4783b 100644
--- a/internal/encoding/ssh/filexfer/buffer.go
+++ b/internal/encoding/ssh/filexfer/buffer.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"encoding/binary"
@@ -17,6 +17,7 @@ var (
 type Buffer struct {
 	b   []byte
 	off int
+	Err error
 }
 
 // NewBuffer creates and initializes a new buffer using buf as its initial contents.
@@ -51,14 +52,17 @@ 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.b = b.b[:0]
-	b.off = 0
+	*b = Buffer{
+		b: b.b[: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.b, b.off = append(b.b[:0], make([]byte, 4)...), 0
+	*b = Buffer{
+		b: append(b.b[:0], make([]byte, 4)...),
+	}
 
 	b.AppendUint8(uint8(packetType))
 	b.AppendUint32(requestID)
@@ -81,15 +85,21 @@ 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 return ErrShortPacket.
-func (b *Buffer) ConsumeUint8() (uint8, error) {
+// 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 b.Len() < 1 {
-		return 0, ErrShortPacket
+		b.off = len(b.b)
+		b.Err = ErrShortPacket
+		return 0
 	}
 
 	var v uint8
 	v, b.off = b.b[b.off], b.off+1
-	return v, nil
+	return v
 }
 
 // AppendUint8 appends a single byte into the buffer.
@@ -98,14 +108,9 @@ 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 return ErrShortPacket.
-func (b *Buffer) ConsumeBool() (bool, error) {
-	v, err := b.ConsumeUint8()
-	if err != nil {
-		return false, err
-	}
-
-	return v != 0, nil
+// If the buffer does not have enough data, it will set Err to ErrShortPacket.
+func (b *Buffer) ConsumeBool() bool {
+	return b.ConsumeUint8() != 0
 }
 
 // AppendBool appends a single bool into the buffer.
@@ -119,15 +124,21 @@ 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 return ErrShortPacket.
-func (b *Buffer) ConsumeUint16() (uint16, error) {
+// 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 b.Len() < 2 {
-		return 0, ErrShortPacket
+		b.off = len(b.b)
+		b.Err = ErrShortPacket
+		return 0
 	}
 
 	v := binary.BigEndian.Uint16(b.b[b.off:])
 	b.off += 2
-	return v, nil
+	return v
 }
 
 // AppendUint16 appends single uint16 into the buffer, in network byte order (big-endian).
@@ -146,15 +157,21 @@ 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 return ErrShortPacket.
-func (b *Buffer) ConsumeUint32() (uint32, error) {
+// 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 b.Len() < 4 {
-		return 0, ErrShortPacket
+		b.off = len(b.b)
+		b.Err = ErrShortPacket
+		return 0
 	}
 
 	v := binary.BigEndian.Uint32(b.b[b.off:])
 	b.off += 4
-	return v, nil
+	return v
 }
 
 // AppendUint32 appends a single uint32 into the buffer, in network byte order (big-endian).
@@ -167,16 +184,33 @@ 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 return ErrShortPacket.
-func (b *Buffer) ConsumeUint64() (uint64, error) {
+// 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 b.Len() < 8 {
-		return 0, ErrShortPacket
+		b.off = len(b.b)
+		b.Err = ErrShortPacket
+		return 0
 	}
 
 	v := binary.BigEndian.Uint64(b.b[b.off:])
 	b.off += 8
-	return v, nil
+	return v
 }
 
 // AppendUint64 appends a single uint64 into the buffer, in network byte order (big-endian).
@@ -194,14 +228,9 @@ 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 return ErrShortPacket.
-func (b *Buffer) ConsumeInt64() (int64, error) {
-	u, err := b.ConsumeUint64()
-	if err != nil {
-		return 0, err
-	}
-
-	return int64(u), err
+// If the buffer does not have enough data, it will set Err to ErrShortPacket.
+func (b *Buffer) ConsumeInt64() int64 {
+	return int64(b.ConsumeUint64())
 }
 
 // AppendInt64 appends a single int64 into the buffer, in network byte order (big-endian) with two’s complement.
@@ -211,29 +240,52 @@ 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 return ErrShortPacket.
+// If the buffer does not have enough data, or defines a length larger than available, it will set Err to 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, error) {
-	length, err := b.ConsumeUint32()
-	if err != nil {
-		return nil, err
+func (b *Buffer) ConsumeByteSlice() []byte {
+	length := int(b.ConsumeUint32())
+	if b.Err != nil {
+		return nil
 	}
 
-	if b.Len() < int(length) {
-		return nil, ErrShortPacket
+	if b.Len() < length || length < 0 {
+		b.off = len(b.b)
+		b.Err = ErrShortPacket
+		return nil
 	}
 
 	v := b.b[b.off:]
-	if len(v) > int(length) {
+	if len(v) > length || cap(v) > length {
 		v = v[:length:length]
 	}
 	b.off += int(length)
-	return v, nil
+	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
 }
 
 // AppendByteSlice appends a single string of raw binary data into the buffer.
@@ -245,17 +297,12 @@ 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 return ErrShortPacket.
+// If the buffer does not have enough data, or defines a length larger than available, it will set Err to 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, error) {
-	v, err := b.ConsumeByteSlice()
-	if err != nil {
-		return "", err
-	}
-
-	return string(v), nil
+func (b *Buffer) ConsumeString() string {
+	return string(b.ConsumeByteSlice())
 }
 
 // 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 6b7b2ce..f717425 100644
--- a/internal/encoding/ssh/filexfer/extended_packets.go
+++ b/internal/encoding/ssh/filexfer/extended_packets.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"encoding"
@@ -86,8 +86,9 @@ 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) {
-	if p.ExtendedRequest, err = buf.ConsumeString(); err != nil {
-		return err
+	p.ExtendedRequest = buf.ConsumeString()
+	if buf.Err != nil {
+		return buf.Err
 	}
 
 	if p.Data == nil {
diff --git a/internal/encoding/ssh/filexfer/extensions.go b/internal/encoding/ssh/filexfer/extensions.go
index 11c0b99..c425780 100644
--- a/internal/encoding/ssh/filexfer/extensions.go
+++ b/internal/encoding/ssh/filexfer/extensions.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // 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,15 +29,12 @@ func (e *ExtensionPair) MarshalBinary() ([]byte, error) {
 
 // UnmarshalFrom unmarshals an ExtensionPair from the given Buffer into e.
 func (e *ExtensionPair) UnmarshalFrom(buf *Buffer) (err error) {
-	if e.Name, err = buf.ConsumeString(); err != nil {
-		return err
+	*e = ExtensionPair{
+		Name: buf.ConsumeString(),
+		Data: buf.ConsumeString(),
 	}
 
-	if e.Data, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // 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 1e5abf7..d300999 100644
--- a/internal/encoding/ssh/filexfer/filexfer.go
+++ b/internal/encoding/ssh/filexfer/filexfer.go
@@ -1,5 +1,5 @@
-// Package filexfer implements the wire encoding for secsh-filexfer as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
-package filexfer
+// 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
 
 // 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 48f8698..9abcbaf 100644
--- a/internal/encoding/ssh/filexfer/fx.go
+++ b/internal/encoding/ssh/filexfer/fx.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"fmt"
@@ -10,7 +10,7 @@ type Status uint32
 // Defines the various SSH_FX_* values.
 const (
 	// see draft-ietf-secsh-filexfer-02
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-7
 	StatusOK = Status(iota)
 	StatusEOF
 	StatusNoSuchFile
@@ -21,28 +21,28 @@ const (
 	StatusConnectionLost
 	StatusOPUnsupported
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-7
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-03.txt#section-7
 	StatusV4InvalidHandle
 	StatusV4NoSuchPath
 	StatusV4FileAlreadyExists
 	StatusV4WriteProtect
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-7
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-04.txt#section-7
 	StatusV4NoMedia
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-7
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-05.txt#section-7
 	StatusV5NoSpaceOnFilesystem
 	StatusV5QuotaExceeded
 	StatusV5UnknownPrincipal
 	StatusV5LockConflict
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-06#section-8
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-06.txt#section-8
 	StatusV6DirNotEmpty
 	StatusV6NotADirectory
 	StatusV6InvalidFilename
 	StatusV6LinkLoop
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-8
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-07.txt#section-8
 	StatusV6CannotDelete
 	StatusV6InvalidParameter
 	StatusV6FileIsADirectory
@@ -50,10 +50,10 @@ const (
 	StatusV6ByteRangeLockRefused
 	StatusV6DeletePending
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-8.1
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-08.txt#section-8.1
 	StatusV6FileCorrupt
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-10#section-9.1
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-10.txt#section-9.1
 	StatusV6OwnerInvalid
 	StatusV6GroupInvalid
 
diff --git a/internal/encoding/ssh/filexfer/fxp.go b/internal/encoding/ssh/filexfer/fxp.go
index 15caf6d..7808002 100644
--- a/internal/encoding/ssh/filexfer/fxp.go
+++ b/internal/encoding/ssh/filexfer/fxp.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"fmt"
@@ -9,7 +9,7 @@ type PacketType uint8
 
 // Request packet types.
 const (
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
 	PacketTypeInit = PacketType(iota + 1)
 	PacketTypeVersion
 	PacketTypeOpen
@@ -31,17 +31,17 @@ const (
 	PacketTypeReadLink
 	PacketTypeSymlink
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-3.3
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-07.txt#section-3.3
 	PacketTypeV6Link
 
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-3.3
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-08.txt#section-3.3
 	PacketTypeV6Block
 	PacketTypeV6Unblock
 )
 
 // Response packet types.
 const (
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
 	PacketTypeStatus = PacketType(iota + 101)
 	PacketTypeHandle
 	PacketTypeData
@@ -51,7 +51,7 @@ const (
 
 // Extended packet types.
 const (
-	// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
+	// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
 	PacketTypeExtended = PacketType(iota + 200)
 	PacketTypeExtendedReply
 )
@@ -122,3 +122,48 @@ 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 a142771..44594ac 100644
--- a/internal/encoding/ssh/filexfer/handle_packets.go
+++ b/internal/encoding/ssh/filexfer/handle_packets.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // 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) {
-	if p.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = ClosePacket{
+		Handle: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // ReadPacket defines the SSH_FXP_READ packet.
 type ReadPacket struct {
 	Handle string
 	Offset uint64
-	Len    uint32
+	Length 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.Len)
+	buf.AppendUint32(p.Length)
 
 	return buf.Packet(payload)
 }
@@ -66,19 +66,13 @@ 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) {
-	if p.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = ReadPacket{
+		Handle: buf.ConsumeString(),
+		Offset: buf.ConsumeUint64(),
+		Length: buf.ConsumeUint32(),
 	}
 
-	if p.Offset, err = buf.ConsumeUint64(); err != nil {
-		return err
-	}
-
-	if p.Len, err = buf.ConsumeUint32(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // WritePacket defines the SSH_FXP_WRITE packet.
@@ -121,26 +115,13 @@ 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) {
-	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))
+	*p = WritePacket{
+		Handle: buf.ConsumeString(),
+		Offset: buf.ConsumeUint64(),
+		Data:   buf.ConsumeByteSliceCopy(p.Data),
 	}
 
-	n := copy(p.Data, data)
-	p.Data = p.Data[:n]
-	return nil
+	return buf.Err
 }
 
 // FStatPacket defines the SSH_FXP_FSTAT packet.
@@ -170,11 +151,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) {
-	if p.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = FStatPacket{
+		Handle: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // FSetstatPacket defines the SSH_FXP_FSETSTAT packet.
@@ -207,8 +188,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) {
-	if p.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = FSetstatPacket{
+		Handle: buf.ConsumeString(),
 	}
 
 	return p.Attrs.UnmarshalFrom(buf)
@@ -241,9 +222,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) {
-	if p.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = ReadDirPacket{
+		Handle: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
diff --git a/internal/encoding/ssh/filexfer/init_packets.go b/internal/encoding/ssh/filexfer/init_packets.go
index b0bc6f5..c553ee2 100644
--- a/internal/encoding/ssh/filexfer/init_packets.go
+++ b/internal/encoding/ssh/filexfer/init_packets.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // 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)
 
-	if p.Version, err = buf.ConsumeUint32(); err != nil {
-		return err
+	*p = InitPacket{
+		Version: buf.ConsumeUint32(),
 	}
 
 	for buf.Len() > 0 {
@@ -46,7 +46,7 @@ func (p *InitPacket) UnmarshalBinary(data []byte) (err error) {
 		p.Extensions = append(p.Extensions, &ext)
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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)
 
-	if p.Version, err = buf.ConsumeUint32(); err != nil {
-		return err
+	*p = VersionPacket{
+		Version: buf.ConsumeUint32(),
 	}
 
 	for buf.Len() > 0 {
diff --git a/internal/encoding/ssh/filexfer/open_packets.go b/internal/encoding/ssh/filexfer/open_packets.go
index 1358711..896ba16 100644
--- a/internal/encoding/ssh/filexfer/open_packets.go
+++ b/internal/encoding/ssh/filexfer/open_packets.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // SSH_FXF_* flags.
 const (
@@ -43,12 +43,9 @@ 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) {
-	if p.Filename, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	if p.PFlags, err = buf.ConsumeUint32(); err != nil {
-		return err
+	*p = OpenPacket{
+		Filename: buf.ConsumeString(),
+		PFlags:   buf.ConsumeUint32(),
 	}
 
 	return p.Attrs.UnmarshalFrom(buf)
@@ -81,9 +78,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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = OpenDirPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
diff --git a/internal/encoding/ssh/filexfer/openssh/fsync.go b/internal/encoding/ssh/filexfer/openssh/fsync.go
index 7ecfb0c..708a4ba 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) {
-	if ep.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*ep = FSyncExtendedPacket{
+		Handle: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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 17c3499..f48d25a 100644
--- a/internal/encoding/ssh/filexfer/openssh/hardlink.go
+++ b/internal/encoding/ssh/filexfer/openssh/hardlink.go
@@ -62,15 +62,12 @@ 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) {
-	if ep.OldPath, err = buf.ConsumeString(); err != nil {
-		return err
+	*ep = HardlinkExtendedPacket{
+		OldPath: buf.ConsumeString(),
+		NewPath: buf.ConsumeString(),
 	}
 
-	if ep.NewPath, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // 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 a3d3de5..5166489 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,19 +61,16 @@ 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) {
-	if ep.OldPath, err = buf.ConsumeString(); err != nil {
-		return err
+func (ep *POSIXRenameExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
+	*ep = POSIXRenameExtendedPacket{
+		OldPath: buf.ConsumeString(),
+		NewPath: buf.ConsumeString(),
 	}
 
-	if ep.NewPath, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // 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 3e9015f..51029ca 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) {
-	if ep.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*ep = StatVFSExtendedPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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) {
-	if ep.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*ep = FStatVFSExtendedPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
@@ -213,41 +213,21 @@ 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) {
-	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
+	*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
 }
 
 // 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 3f24e9c..fdf65d0 100644
--- a/internal/encoding/ssh/filexfer/packets.go
+++ b/internal/encoding/ssh/filexfer/packets.go
@@ -1,59 +1,13 @@
-package filexfer
+package sshfx
 
 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,
@@ -63,7 +17,7 @@ func newPacketFromType(typ PacketType) (Packet, error) {
 // For servers expecting to receive arbitrary request packet types,
 // use RequestPacket.
 //
-// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
+// Defined in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
 type RawPacket struct {
 	PacketType PacketType
 	RequestID  uint32
@@ -110,19 +64,14 @@ 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 {
-	typ, err := buf.ConsumeUint8()
-	if err != nil {
-		return err
-	}
-
-	p.PacketType = PacketType(typ)
-
-	if p.RequestID, err = buf.ConsumeUint32(); err != nil {
-		return err
+	*p = RawPacket{
+		PacketType: PacketType(buf.ConsumeUint8()),
+		RequestID:  buf.ConsumeUint32(),
 	}
 
 	p.Data = *buf
-	return nil
+
+	return buf.Err
 }
 
 // UnmarshalBinary decodes a full raw packet out of the given data.
@@ -225,7 +174,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://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
+// Defined in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
 type RequestPacket struct {
 	RequestID uint32
 
@@ -268,18 +217,19 @@ 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, err := buf.ConsumeUint8()
-	if err != nil {
-		return err
+	typ := PacketType(buf.ConsumeUint8())
+	if buf.Err != nil {
+		return buf.Err
 	}
 
-	p.Request, err = newPacketFromType(PacketType(typ))
+	req, err := newPacketFromType(typ)
 	if err != nil {
 		return err
 	}
 
-	if p.RequestID, err = buf.ConsumeUint32(); err != nil {
-		return err
+	*p = RequestPacket{
+		RequestID: buf.ConsumeUint32(),
+		Request:   req,
 	}
 
 	return p.Request.UnmarshalPacketBody(buf)
diff --git a/internal/encoding/ssh/filexfer/path_packets.go b/internal/encoding/ssh/filexfer/path_packets.go
index 5792d47..0180326 100644
--- a/internal/encoding/ssh/filexfer/path_packets.go
+++ b/internal/encoding/ssh/filexfer/path_packets.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // 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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = LStatPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = SetstatPacket{
+		Path: buf.ConsumeString(),
 	}
 
 	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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = RemovePacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = MkdirPacket{
+		Path: buf.ConsumeString(),
 	}
 
 	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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = RmdirPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = RealPathPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // 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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = StatPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // RenamePacket defines the SSH_FXP_RENAME packet.
@@ -274,15 +274,12 @@ 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) {
-	if p.OldPath, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = RenamePacket{
+		OldPath: buf.ConsumeString(),
+		NewPath: buf.ConsumeString(),
 	}
 
-	if p.NewPath, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // ReadLinkPacket defines the SSH_FXP_READLINK packet.
@@ -312,11 +309,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) {
-	if p.Path, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = ReadLinkPacket{
+		Path: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // SymlinkPacket defines the SSH_FXP_SYMLINK packet.
@@ -355,14 +352,11 @@ 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) {
-	// Arguments were inadvertently reversed.
-	if p.TargetPath, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	if p.LinkPath, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = SymlinkPacket{
+		// Arguments were inadvertently reversed.
+		TargetPath: buf.ConsumeString(),
+		LinkPath:   buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
diff --git a/internal/encoding/ssh/filexfer/permissions.go b/internal/encoding/ssh/filexfer/permissions.go
index 2fe63d5..0143ec0 100644
--- a/internal/encoding/ssh/filexfer/permissions.go
+++ b/internal/encoding/ssh/filexfer/permissions.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 // 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 7a9b3ea..311708f 100644
--- a/internal/encoding/ssh/filexfer/response_packets.go
+++ b/internal/encoding/ssh/filexfer/response_packets.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"fmt"
@@ -6,7 +6,7 @@ import (
 
 // StatusPacket defines the SSH_FXP_STATUS packet.
 //
-// Specified in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7
+// Specified in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#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: %q (%s)", p.ErrorMessage, p.StatusCode)
+	return fmt.Sprintf("sftp: %s: %q", p.StatusCode, p.ErrorMessage)
 }
 
 // Is returns true if target is a StatusPacket with the same StatusCode,
@@ -57,21 +57,13 @@ 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) {
-	statusCode, err := buf.ConsumeUint32()
-	if err != nil {
-		return err
+	*p = StatusPacket{
+		StatusCode:   Status(buf.ConsumeUint32()),
+		ErrorMessage: buf.ConsumeString(),
+		LanguageTag:  buf.ConsumeString(),
 	}
-	p.StatusCode = Status(statusCode)
 
-	if p.ErrorMessage, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	if p.LanguageTag, err = buf.ConsumeString(); err != nil {
-		return err
-	}
-
-	return nil
+	return buf.Err
 }
 
 // HandlePacket defines the SSH_FXP_HANDLE packet.
@@ -101,11 +93,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) {
-	if p.Handle, err = buf.ConsumeString(); err != nil {
-		return err
+	*p = HandlePacket{
+		Handle: buf.ConsumeString(),
 	}
 
-	return nil
+	return buf.Err
 }
 
 // DataPacket defines the SSH_FXP_DATA packet.
@@ -143,18 +135,11 @@ 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) {
-	data, err := buf.ConsumeByteSlice()
-	if err != nil {
-		return err
+	*p = DataPacket{
+		Data: buf.ConsumeByteSliceCopy(p.Data),
 	}
 
-	if len(p.Data) < len(data) {
-		p.Data = make([]byte, len(data))
-	}
-
-	n := copy(p.Data, data)
-	p.Data = p.Data[:n]
-	return nil
+	return buf.Err
 }
 
 // NamePacket defines the SSH_FXP_NAME packet.
@@ -193,14 +178,16 @@ 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, err := buf.ConsumeUint32()
-	if err != nil {
-		return err
+	count := buf.ConsumeCount()
+	if buf.Err != nil {
+		return buf.Err
 	}
 
-	p.Entries = make([]*NameEntry, 0, count)
+	*p = NamePacket{
+		Entries: make([]*NameEntry, 0, count),
+	}
 
-	for i := uint32(0); i < count; i++ {
+	for i := 0; i < count; i++ {
 		var e NameEntry
 		if err := e.UnmarshalFrom(buf); err != nil {
 			return err
@@ -209,7 +196,7 @@ func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
 		p.Entries = append(p.Entries, &e)
 	}
 
-	return nil
+	return buf.Err
 }
 
 // AttrsPacket defines the SSH_FXP_ATTRS packet.
diff --git a/ls_plan9.go b/ls_plan9.go
index a16a3ea..b70b294 100644
--- a/ls_plan9.go
+++ b/ls_plan9.go
@@ -1,3 +1,4 @@
+//go:build plan9
 // +build plan9
 
 package sftp
diff --git a/ls_stub.go b/ls_stub.go
index 6dec393..f58abf7 100644
--- a/ls_stub.go
+++ b/ls_stub.go
@@ -1,3 +1,4 @@
+//go:build windows || android
 // +build windows android
 
 package sftp
diff --git a/ls_unix.go b/ls_unix.go
index 59ccffd..0beba32 100644
--- a/ls_unix.go
+++ b/ls_unix.go
@@ -1,3 +1,4 @@
+//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 c740c4c..647836b 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 f4f9052..fec88e7 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/release.go b/release.go
index b695528..9ecedc4 100644
--- a/release.go
+++ b/release.go
@@ -1,3 +1,4 @@
+//go:build !debug
 // +build !debug
 
 package sftp
diff --git a/request-plan9.go b/request-plan9.go
index 0e3b483..38f91bc 100644
--- a/request-plan9.go
+++ b/request-plan9.go
@@ -1,3 +1,4 @@
+//go:build plan9
 // +build plan9
 
 package sftp
diff --git a/request-readme.md b/request-readme.md
index f887274..f8b81f3 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://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.3) for
+spec](https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-6.3) for
 details.
 
 ###    Filecmd(*Request) error
diff --git a/request-unix.go b/request-unix.go
index d30b256..e3e037d 100644
--- a/request-unix.go
+++ b/request-unix.go
@@ -1,3 +1,4 @@
+//go:build !windows && !plan9
 // +build !windows,!plan9
 
 package sftp
diff --git a/server.go b/server.go
index a09741c..503454e 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 http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
+// as specified at https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
 type Server struct {
 	*serverConn
 	debugStream   io.Writer
diff --git a/server_statvfs_impl.go b/server_statvfs_impl.go
index 94b6d83..a547079 100644
--- a/server_statvfs_impl.go
+++ b/server_statvfs_impl.go
@@ -1,3 +1,4 @@
+//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 1d180d4..615c415 100644
--- a/server_statvfs_linux.go
+++ b/server_statvfs_linux.go
@@ -1,3 +1,4 @@
+//go:build linux
 // +build linux
 
 package sftp
diff --git a/server_statvfs_stubs.go b/server_statvfs_stubs.go
index fbf4906..dd4705b 100644
--- a/server_statvfs_stubs.go
+++ b/server_statvfs_stubs.go
@@ -1,3 +1,4 @@
+//go:build !darwin && !linux && !plan9
 // +build !darwin,!linux,!plan9
 
 package sftp
diff --git a/sftp.go b/sftp.go
index 9a63c39..778c8f3 100644
--- a/sftp.go
+++ b/sftp.go
@@ -1,5 +1,5 @@
 // Package sftp implements the SSH File Transfer Protocol as described in
-// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
+// https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
 package sftp
 
 import (
diff --git a/syscall_fixed.go b/syscall_fixed.go
index d404577..e844308 100644
--- a/syscall_fixed.go
+++ b/syscall_fixed.go
@@ -1,3 +1,4 @@
+//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 4c2b240..5005218 100644
--- a/syscall_good.go
+++ b/syscall_good.go
@@ -1,4 +1,6 @@
-// +build !plan9,!windows
+//go:build !plan9 && !windows && (!js || !wasm)
+// +build !plan9
+// +build !windows
 // +build !js !wasm
 
 package sftp


Test patch:
diff --git a/internal/encoding/ssh/filexfer/attrs_test.go b/internal/encoding/ssh/filexfer/attrs_test.go
index 6716a57..1e620fa 100644
--- a/internal/encoding/ssh/filexfer/attrs_test.go
+++ b/internal/encoding/ssh/filexfer/attrs_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/extended_packets_test.go b/internal/encoding/ssh/filexfer/extended_packets_test.go
index 0860773..d7a54d3 100644
--- a/internal/encoding/ssh/filexfer/extended_packets_test.go
+++ b/internal/encoding/ssh/filexfer/extended_packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
@@ -20,9 +20,9 @@ func (d *testExtendedData) MarshalBinary() ([]byte, error) {
 func (d *testExtendedData) UnmarshalBinary(data []byte) error {
 	buf := NewBuffer(data)
 
-	v, err := buf.ConsumeUint8()
-	if err != nil {
-		return err
+	v := buf.ConsumeUint8()
+	if buf.Err != nil {
+		return buf.Err
 	}
 
 	d.value = v ^ 0x2a
diff --git a/internal/encoding/ssh/filexfer/extensions_test.go b/internal/encoding/ssh/filexfer/extensions_test.go
index 453265b..15f2a79 100644
--- a/internal/encoding/ssh/filexfer/extensions_test.go
+++ b/internal/encoding/ssh/filexfer/extensions_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/fx_test.go b/internal/encoding/ssh/filexfer/fx_test.go
index 3e8db1d..9548059 100644
--- a/internal/encoding/ssh/filexfer/fx_test.go
+++ b/internal/encoding/ssh/filexfer/fx_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bufio"
@@ -9,7 +9,7 @@ import (
 	"testing"
 )
 
-// This string data is copied verbatim from https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
+// This string data is copied verbatim from https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-13.txt
 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 14e8ff7..b0a3a83 100644
--- a/internal/encoding/ssh/filexfer/fxp_test.go
+++ b/internal/encoding/ssh/filexfer/fxp_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bufio"
@@ -8,7 +8,8 @@ import (
 	"testing"
 )
 
-// This string data is copied verbatim from https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
+// 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.
 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 bd034a1..249ff32 100644
--- a/internal/encoding/ssh/filexfer/handle_packets_test.go
+++ b/internal/encoding/ssh/filexfer/handle_packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
@@ -58,7 +58,7 @@ func TestReadPacket(t *testing.T) {
 	p := &ReadPacket{
 		Handle: "somehandle",
 		Offset: offset,
-		Len:    length,
+		Length: 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.Len != length {
-		t.Errorf("UnmarshalPacketBody(): Len was %x, but expected %x", p.Len, length)
+	if p.Length != length {
+		t.Errorf("UnmarshalPacketBody(): Length was %x, but expected %x", p.Length, length)
 	}
 }
 
diff --git a/internal/encoding/ssh/filexfer/init_packets_test.go b/internal/encoding/ssh/filexfer/init_packets_test.go
index e7605f9..0c466bf 100644
--- a/internal/encoding/ssh/filexfer/init_packets_test.go
+++ b/internal/encoding/ssh/filexfer/init_packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/open_packets_test.go b/internal/encoding/ssh/filexfer/open_packets_test.go
index a6b4ac0..a792bf4 100644
--- a/internal/encoding/ssh/filexfer/open_packets_test.go
+++ b/internal/encoding/ssh/filexfer/open_packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 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 6bdb10d..58e4fff 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 1600920..f6dac8e 100644
--- a/internal/encoding/ssh/filexfer/packets_test.go
+++ b/internal/encoding/ssh/filexfer/packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/path_packets_test.go b/internal/encoding/ssh/filexfer/path_packets_test.go
index bad8528..f016f7e 100644
--- a/internal/encoding/ssh/filexfer/path_packets_test.go
+++ b/internal/encoding/ssh/filexfer/path_packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
diff --git a/internal/encoding/ssh/filexfer/response_packets_test.go b/internal/encoding/ssh/filexfer/response_packets_test.go
index fd00601..3f8c5b0 100644
--- a/internal/encoding/ssh/filexfer/response_packets_test.go
+++ b/internal/encoding/ssh/filexfer/response_packets_test.go
@@ -1,4 +1,4 @@
-package filexfer
+package sshfx
 
 import (
 	"bytes"
diff --git a/ls_formatting_test.go b/ls_formatting_test.go
index c9c63a1..8d0fefb 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