aboutsummaryrefslogtreecommitdiff
path: root/internal/encoding/ssh/filexfer/openssh
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2021-11-19 19:54:49 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2021-11-19 19:54:49 +0100
commit0ee29e31ddcc81f541de7459b0a5e40dfa552672 (patch)
tree859ff133f8c78bd034b0c2184cdad0ce9f38b065 /internal/encoding/ssh/filexfer/openssh
parent93631b4e3d5195d446504db1c4a2bc7468b3ef28 (diff)
downloadbagage-0ee29e31ddcc81f541de7459b0a5e40dfa552672.tar.gz
bagage-0ee29e31ddcc81f541de7459b0a5e40dfa552672.zip
Working on SFTP
Diffstat (limited to 'internal/encoding/ssh/filexfer/openssh')
-rw-r--r--internal/encoding/ssh/filexfer/openssh/fsync.go73
-rw-r--r--internal/encoding/ssh/filexfer/openssh/fsync_test.go62
-rw-r--r--internal/encoding/ssh/filexfer/openssh/hardlink.go79
-rw-r--r--internal/encoding/ssh/filexfer/openssh/hardlink_test.go69
-rw-r--r--internal/encoding/ssh/filexfer/openssh/openssh.go2
-rw-r--r--internal/encoding/ssh/filexfer/openssh/posix-rename.go79
-rw-r--r--internal/encoding/ssh/filexfer/openssh/posix-rename_test.go69
-rw-r--r--internal/encoding/ssh/filexfer/openssh/statvfs.go256
-rw-r--r--internal/encoding/ssh/filexfer/openssh/statvfs_test.go239
9 files changed, 928 insertions, 0 deletions
diff --git a/internal/encoding/ssh/filexfer/openssh/fsync.go b/internal/encoding/ssh/filexfer/openssh/fsync.go
new file mode 100644
index 0000000..7ecfb0c
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/fsync.go
@@ -0,0 +1,73 @@
+package openssh
+
+import (
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+const extensionFSync = "fsync@openssh.com"
+
+// RegisterExtensionFSync registers the "fsync@openssh.com" extended packet with the encoding/ssh/filexfer package.
+func RegisterExtensionFSync() {
+ sshfx.RegisterExtendedPacketType(extensionFSync, func() sshfx.ExtendedData {
+ return new(FSyncExtendedPacket)
+ })
+}
+
+// ExtensionFSync returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
+func ExtensionFSync() *sshfx.ExtensionPair {
+ return &sshfx.ExtensionPair{
+ Name: extensionFSync,
+ Data: "1",
+ }
+}
+
+// FSyncExtendedPacket defines the fsync@openssh.com extend packet.
+type FSyncExtendedPacket struct {
+ Handle string
+}
+
+// Type returns the SSH_FXP_EXTENDED packet type.
+func (ep *FSyncExtendedPacket) Type() sshfx.PacketType {
+ return sshfx.PacketTypeExtended
+}
+
+// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
+func (ep *FSyncExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
+ p := &sshfx.ExtendedPacket{
+ ExtendedRequest: extensionFSync,
+
+ Data: ep,
+ }
+ return p.MarshalPacket(reqid, b)
+}
+
+// MarshalInto encodes ep into the binary encoding of the fsync@openssh.com extended packet-specific data.
+func (ep *FSyncExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
+ buf.AppendString(ep.Handle)
+}
+
+// MarshalBinary encodes ep into the binary encoding of the fsync@openssh.com extended packet-specific data.
+//
+// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
+func (ep *FSyncExtendedPacket) MarshalBinary() ([]byte, error) {
+ // string(handle)
+ size := 4 + len(ep.Handle)
+
+ buf := sshfx.NewBuffer(make([]byte, 0, size))
+ ep.MarshalInto(buf)
+ return buf.Bytes(), nil
+}
+
+// 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
+ }
+
+ return nil
+}
+
+// UnmarshalBinary decodes the fsync@openssh.com extended packet-specific data into ep.
+func (ep *FSyncExtendedPacket) UnmarshalBinary(data []byte) (err error) {
+ return ep.UnmarshalFrom(sshfx.NewBuffer(data))
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/fsync_test.go b/internal/encoding/ssh/filexfer/openssh/fsync_test.go
new file mode 100644
index 0000000..f9e878f
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/fsync_test.go
@@ -0,0 +1,62 @@
+package openssh
+
+import (
+ "bytes"
+ "testing"
+
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+var _ sshfx.PacketMarshaller = &FSyncExtendedPacket{}
+
+func init() {
+ RegisterExtensionFSync()
+}
+
+func TestFSyncExtendedPacket(t *testing.T) {
+ const (
+ id = 42
+ handle = "somehandle"
+ )
+
+ ep := &FSyncExtendedPacket{
+ Handle: handle,
+ }
+
+ data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
+ if err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ want := []byte{
+ 0x00, 0x00, 0x00, 40,
+ 200,
+ 0x00, 0x00, 0x00, 42,
+ 0x00, 0x00, 0x00, 17, 'f', 's', 'y', 'n', 'c', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
+ 0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
+ }
+
+ if !bytes.Equal(data, want) {
+ t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
+ }
+
+ var p sshfx.ExtendedPacket
+
+ // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
+ if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ if p.ExtendedRequest != extensionFSync {
+ t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionFSync)
+ }
+
+ ep, ok := p.Data.(*FSyncExtendedPacket)
+ if !ok {
+ t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *FSyncExtendedPacket", p.Data)
+ }
+
+ if ep.Handle != handle {
+ t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", ep.Handle, handle)
+ }
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/hardlink.go b/internal/encoding/ssh/filexfer/openssh/hardlink.go
new file mode 100644
index 0000000..17c3499
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/hardlink.go
@@ -0,0 +1,79 @@
+package openssh
+
+import (
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+const extensionHardlink = "hardlink@openssh.com"
+
+// RegisterExtensionHardlink registers the "hardlink@openssh.com" extended packet with the encoding/ssh/filexfer package.
+func RegisterExtensionHardlink() {
+ sshfx.RegisterExtendedPacketType(extensionHardlink, func() sshfx.ExtendedData {
+ return new(HardlinkExtendedPacket)
+ })
+}
+
+// ExtensionHardlink returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
+func ExtensionHardlink() *sshfx.ExtensionPair {
+ return &sshfx.ExtensionPair{
+ Name: extensionHardlink,
+ Data: "1",
+ }
+}
+
+// HardlinkExtendedPacket defines the hardlink@openssh.com extend packet.
+type HardlinkExtendedPacket struct {
+ OldPath string
+ NewPath string
+}
+
+// Type returns the SSH_FXP_EXTENDED packet type.
+func (ep *HardlinkExtendedPacket) Type() sshfx.PacketType {
+ return sshfx.PacketTypeExtended
+}
+
+// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
+func (ep *HardlinkExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
+ p := &sshfx.ExtendedPacket{
+ ExtendedRequest: extensionHardlink,
+
+ Data: ep,
+ }
+ return p.MarshalPacket(reqid, b)
+}
+
+// MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
+func (ep *HardlinkExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
+ buf.AppendString(ep.OldPath)
+ buf.AppendString(ep.NewPath)
+}
+
+// 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 *HardlinkExtendedPacket) MarshalBinary() ([]byte, error) {
+ // string(oldpath) + string(newpath)
+ size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath)
+
+ buf := sshfx.NewBuffer(make([]byte, 0, size))
+ ep.MarshalInto(buf)
+ return buf.Bytes(), nil
+}
+
+// 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
+ }
+
+ 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 *HardlinkExtendedPacket) UnmarshalBinary(data []byte) (err error) {
+ return ep.UnmarshalFrom(sshfx.NewBuffer(data))
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/hardlink_test.go b/internal/encoding/ssh/filexfer/openssh/hardlink_test.go
new file mode 100644
index 0000000..5d3be06
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/hardlink_test.go
@@ -0,0 +1,69 @@
+package openssh
+
+import (
+ "bytes"
+ "testing"
+
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+var _ sshfx.PacketMarshaller = &HardlinkExtendedPacket{}
+
+func init() {
+ RegisterExtensionHardlink()
+}
+
+func TestHardlinkExtendedPacket(t *testing.T) {
+ const (
+ id = 42
+ oldpath = "/foo"
+ newpath = "/bar"
+ )
+
+ ep := &HardlinkExtendedPacket{
+ OldPath: oldpath,
+ NewPath: newpath,
+ }
+
+ data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
+ if err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ want := []byte{
+ 0x00, 0x00, 0x00, 45,
+ 200,
+ 0x00, 0x00, 0x00, 42,
+ 0x00, 0x00, 0x00, 20, 'h', 'a', 'r', 'd', 'l', 'i', 'n', 'k', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
+ 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
+ 0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r',
+ }
+
+ if !bytes.Equal(data, want) {
+ t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
+ }
+
+ var p sshfx.ExtendedPacket
+
+ // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
+ if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ if p.ExtendedRequest != extensionHardlink {
+ t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionHardlink)
+ }
+
+ ep, ok := p.Data.(*HardlinkExtendedPacket)
+ if !ok {
+ t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *HardlinkExtendedPacket", p.Data)
+ }
+
+ if ep.OldPath != oldpath {
+ t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", ep.OldPath, oldpath)
+ }
+
+ if ep.NewPath != newpath {
+ t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", ep.NewPath, newpath)
+ }
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/openssh.go b/internal/encoding/ssh/filexfer/openssh/openssh.go
new file mode 100644
index 0000000..f93ff17
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/openssh.go
@@ -0,0 +1,2 @@
+// Package openssh implements the openssh secsh-filexfer extensions as described in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
+package openssh
diff --git a/internal/encoding/ssh/filexfer/openssh/posix-rename.go b/internal/encoding/ssh/filexfer/openssh/posix-rename.go
new file mode 100644
index 0000000..a3d3de5
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/posix-rename.go
@@ -0,0 +1,79 @@
+package openssh
+
+import (
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+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)
+ })
+}
+
+// ExtensionPosixRename returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
+func ExtensionPosixRename() *sshfx.ExtensionPair {
+ return &sshfx.ExtensionPair{
+ Name: extensionPosixRename,
+ Data: "1",
+ }
+}
+
+// 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 {
+ 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) {
+ p := &sshfx.ExtendedPacket{
+ ExtendedRequest: extensionPosixRename,
+
+ Data: ep,
+ }
+ return p.MarshalPacket(reqid, b)
+}
+
+// MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
+func (ep *PosixRenameExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
+ buf.AppendString(ep.OldPath)
+ buf.AppendString(ep.NewPath)
+}
+
+// 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) {
+ // string(oldpath) + string(newpath)
+ size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath)
+
+ buf := sshfx.NewBuffer(make([]byte, 0, size))
+ ep.MarshalInto(buf)
+ return buf.Bytes(), nil
+}
+
+// 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
+ }
+
+ 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) {
+ return ep.UnmarshalFrom(sshfx.NewBuffer(data))
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go b/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
new file mode 100644
index 0000000..6bdb10d
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
@@ -0,0 +1,69 @@
+package openssh
+
+import (
+ "bytes"
+ "testing"
+
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+var _ sshfx.PacketMarshaller = &PosixRenameExtendedPacket{}
+
+func init() {
+ RegisterExtensionPosixRename()
+}
+
+func TestPosixRenameExtendedPacket(t *testing.T) {
+ const (
+ id = 42
+ oldpath = "/foo"
+ newpath = "/bar"
+ )
+
+ ep := &PosixRenameExtendedPacket{
+ OldPath: oldpath,
+ NewPath: newpath,
+ }
+
+ data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
+ if err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ want := []byte{
+ 0x00, 0x00, 0x00, 49,
+ 200,
+ 0x00, 0x00, 0x00, 42,
+ 0x00, 0x00, 0x00, 24, 'p', 'o', 's', 'i', 'x', '-', 'r', 'e', 'n', 'a', 'm', 'e', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
+ 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
+ 0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r',
+ }
+
+ if !bytes.Equal(data, want) {
+ t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
+ }
+
+ var p sshfx.ExtendedPacket
+
+ // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
+ if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ if p.ExtendedRequest != extensionPosixRename {
+ t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionPosixRename)
+ }
+
+ ep, ok := p.Data.(*PosixRenameExtendedPacket)
+ if !ok {
+ t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *PosixRenameExtendedPacket", p.Data)
+ }
+
+ if ep.OldPath != oldpath {
+ t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", ep.OldPath, oldpath)
+ }
+
+ if ep.NewPath != newpath {
+ t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", ep.NewPath, newpath)
+ }
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/statvfs.go b/internal/encoding/ssh/filexfer/openssh/statvfs.go
new file mode 100644
index 0000000..3e9015f
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/statvfs.go
@@ -0,0 +1,256 @@
+package openssh
+
+import (
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+const extensionStatVFS = "statvfs@openssh.com"
+
+// RegisterExtensionStatVFS registers the "statvfs@openssh.com" extended packet with the encoding/ssh/filexfer package.
+func RegisterExtensionStatVFS() {
+ sshfx.RegisterExtendedPacketType(extensionStatVFS, func() sshfx.ExtendedData {
+ return new(StatVFSExtendedPacket)
+ })
+}
+
+// ExtensionStatVFS returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
+func ExtensionStatVFS() *sshfx.ExtensionPair {
+ return &sshfx.ExtensionPair{
+ Name: extensionStatVFS,
+ Data: "2",
+ }
+}
+
+// StatVFSExtendedPacket defines the statvfs@openssh.com extend packet.
+type StatVFSExtendedPacket struct {
+ Path string
+}
+
+// Type returns the SSH_FXP_EXTENDED packet type.
+func (ep *StatVFSExtendedPacket) Type() sshfx.PacketType {
+ return sshfx.PacketTypeExtended
+}
+
+// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
+func (ep *StatVFSExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
+ p := &sshfx.ExtendedPacket{
+ ExtendedRequest: extensionStatVFS,
+
+ Data: ep,
+ }
+ return p.MarshalPacket(reqid, b)
+}
+
+// MarshalInto encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
+func (ep *StatVFSExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
+ buf.AppendString(ep.Path)
+}
+
+// MarshalBinary encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
+//
+// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
+func (ep *StatVFSExtendedPacket) MarshalBinary() ([]byte, error) {
+ size := 4 + len(ep.Path) // string(path)
+
+ buf := sshfx.NewBuffer(make([]byte, 0, size))
+
+ ep.MarshalInto(buf)
+
+ return buf.Bytes(), nil
+}
+
+// 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
+ }
+
+ return nil
+}
+
+// UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
+func (ep *StatVFSExtendedPacket) UnmarshalBinary(data []byte) (err error) {
+ return ep.UnmarshalFrom(sshfx.NewBuffer(data))
+}
+
+const extensionFStatVFS = "fstatvfs@openssh.com"
+
+// RegisterExtensionFStatVFS registers the "fstatvfs@openssh.com" extended packet with the encoding/ssh/filexfer package.
+func RegisterExtensionFStatVFS() {
+ sshfx.RegisterExtendedPacketType(extensionFStatVFS, func() sshfx.ExtendedData {
+ return new(FStatVFSExtendedPacket)
+ })
+}
+
+// ExtensionFStatVFS returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
+func ExtensionFStatVFS() *sshfx.ExtensionPair {
+ return &sshfx.ExtensionPair{
+ Name: extensionFStatVFS,
+ Data: "2",
+ }
+}
+
+// FStatVFSExtendedPacket defines the fstatvfs@openssh.com extend packet.
+type FStatVFSExtendedPacket struct {
+ Path string
+}
+
+// Type returns the SSH_FXP_EXTENDED packet type.
+func (ep *FStatVFSExtendedPacket) Type() sshfx.PacketType {
+ return sshfx.PacketTypeExtended
+}
+
+// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
+func (ep *FStatVFSExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
+ p := &sshfx.ExtendedPacket{
+ ExtendedRequest: extensionFStatVFS,
+
+ Data: ep,
+ }
+ return p.MarshalPacket(reqid, b)
+}
+
+// MarshalInto encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
+func (ep *FStatVFSExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
+ buf.AppendString(ep.Path)
+}
+
+// MarshalBinary encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
+//
+// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
+func (ep *FStatVFSExtendedPacket) MarshalBinary() ([]byte, error) {
+ size := 4 + len(ep.Path) // string(path)
+
+ buf := sshfx.NewBuffer(make([]byte, 0, size))
+
+ ep.MarshalInto(buf)
+
+ return buf.Bytes(), nil
+}
+
+// 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
+ }
+
+ return nil
+}
+
+// UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
+func (ep *FStatVFSExtendedPacket) UnmarshalBinary(data []byte) (err error) {
+ return ep.UnmarshalFrom(sshfx.NewBuffer(data))
+}
+
+// The values for the MountFlags field.
+// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
+const (
+ MountFlagsReadOnly = 0x1 // SSH_FXE_STATVFS_ST_RDONLY
+ MountFlagsNoSUID = 0x2 // SSH_FXE_STATVFS_ST_NOSUID
+)
+
+// StatVFSExtendedReplyPacket defines the extended reply packet for statvfs@openssh.com and fstatvfs@openssh.com requests.
+type StatVFSExtendedReplyPacket struct {
+ BlockSize uint64 /* f_bsize: file system block size */
+ FragmentSize uint64 /* f_frsize: fundamental fs block size / fagment size */
+ Blocks uint64 /* f_blocks: number of blocks (unit f_frsize) */
+ BlocksFree uint64 /* f_bfree: free blocks in filesystem */
+ BlocksAvail uint64 /* f_bavail: free blocks for non-root */
+ Files uint64 /* f_files: total file inodes */
+ FilesFree uint64 /* f_ffree: free file inodes */
+ FilesAvail uint64 /* f_favail: free file inodes for to non-root */
+ FilesystemID uint64 /* f_fsid: file system id */
+ MountFlags uint64 /* f_flag: bit mask of mount flag values */
+ MaxNameLength uint64 /* f_namemax: maximum filename length */
+}
+
+// Type returns the SSH_FXP_EXTENDED_REPLY packet type.
+func (ep *StatVFSExtendedReplyPacket) Type() sshfx.PacketType {
+ return sshfx.PacketTypeExtendedReply
+}
+
+// MarshalPacket returns ep as a two-part binary encoding of the full extended reply packet.
+func (ep *StatVFSExtendedReplyPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
+ p := &sshfx.ExtendedReplyPacket{
+ Data: ep,
+ }
+ return p.MarshalPacket(reqid, b)
+}
+
+// UnmarshalPacketBody returns ep as a two-part binary encoding of the full extended reply packet.
+func (ep *StatVFSExtendedReplyPacket) UnmarshalPacketBody(buf *sshfx.Buffer) (err error) {
+ p := &sshfx.ExtendedReplyPacket{
+ Data: ep,
+ }
+ return p.UnmarshalPacketBody(buf)
+}
+
+// MarshalInto encodes ep into the binary encoding of the (f)statvfs@openssh.com extended reply packet-specific data.
+func (ep *StatVFSExtendedReplyPacket) MarshalInto(buf *sshfx.Buffer) {
+ buf.AppendUint64(ep.BlockSize)
+ buf.AppendUint64(ep.FragmentSize)
+ buf.AppendUint64(ep.Blocks)
+ buf.AppendUint64(ep.BlocksFree)
+ buf.AppendUint64(ep.BlocksAvail)
+ buf.AppendUint64(ep.Files)
+ buf.AppendUint64(ep.FilesFree)
+ buf.AppendUint64(ep.FilesAvail)
+ buf.AppendUint64(ep.FilesystemID)
+ buf.AppendUint64(ep.MountFlags)
+ buf.AppendUint64(ep.MaxNameLength)
+}
+
+// MarshalBinary encodes ep into the binary encoding of the (f)statvfs@openssh.com extended reply packet-specific data.
+//
+// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended reply packet.
+func (ep *StatVFSExtendedReplyPacket) MarshalBinary() ([]byte, error) {
+ size := 11 * 8 // 11 × uint64(various)
+
+ b := sshfx.NewBuffer(make([]byte, 0, size))
+ ep.MarshalInto(b)
+ return b.Bytes(), nil
+}
+
+// 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
+}
+
+// UnmarshalBinary decodes the fstatvfs@openssh.com extended reply packet-specific data into ep.
+func (ep *StatVFSExtendedReplyPacket) UnmarshalBinary(data []byte) (err error) {
+ return ep.UnmarshalFrom(sshfx.NewBuffer(data))
+}
diff --git a/internal/encoding/ssh/filexfer/openssh/statvfs_test.go b/internal/encoding/ssh/filexfer/openssh/statvfs_test.go
new file mode 100644
index 0000000..014aa63
--- /dev/null
+++ b/internal/encoding/ssh/filexfer/openssh/statvfs_test.go
@@ -0,0 +1,239 @@
+package openssh
+
+import (
+ "bytes"
+ "testing"
+
+ sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
+)
+
+var _ sshfx.PacketMarshaller = &StatVFSExtendedPacket{}
+
+func init() {
+ RegisterExtensionStatVFS()
+}
+
+func TestStatVFSExtendedPacket(t *testing.T) {
+ const (
+ id = 42
+ path = "/foo"
+ )
+
+ ep := &StatVFSExtendedPacket{
+ Path: path,
+ }
+
+ data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
+ if err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ want := []byte{
+ 0x00, 0x00, 0x00, 36,
+ 200,
+ 0x00, 0x00, 0x00, 42,
+ 0x00, 0x00, 0x00, 19, 's', 't', 'a', 't', 'v', 'f', 's', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
+ 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
+ }
+
+ if !bytes.Equal(data, want) {
+ t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
+ }
+
+ var p sshfx.ExtendedPacket
+
+ // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
+ if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ if p.ExtendedRequest != extensionStatVFS {
+ t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionStatVFS)
+ }
+
+ ep, ok := p.Data.(*StatVFSExtendedPacket)
+ if !ok {
+ t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *StatVFSExtendedPacket", p.Data)
+ }
+
+ if ep.Path != path {
+ t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", ep.Path, path)
+ }
+}
+
+var _ sshfx.PacketMarshaller = &FStatVFSExtendedPacket{}
+
+func init() {
+ RegisterExtensionFStatVFS()
+}
+
+func TestFStatVFSExtendedPacket(t *testing.T) {
+ const (
+ id = 42
+ path = "/foo"
+ )
+
+ ep := &FStatVFSExtendedPacket{
+ Path: path,
+ }
+
+ data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
+ if err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ want := []byte{
+ 0x00, 0x00, 0x00, 37,
+ 200,
+ 0x00, 0x00, 0x00, 42,
+ 0x00, 0x00, 0x00, 20, 'f', 's', 't', 'a', 't', 'v', 'f', 's', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
+ 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
+ }
+
+ if !bytes.Equal(data, want) {
+ t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
+ }
+
+ var p sshfx.ExtendedPacket
+
+ // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
+ if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ if p.ExtendedRequest != extensionFStatVFS {
+ t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionFStatVFS)
+ }
+
+ ep, ok := p.Data.(*FStatVFSExtendedPacket)
+ if !ok {
+ t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *FStatVFSExtendedPacket", p.Data)
+ }
+
+ if ep.Path != path {
+ t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", ep.Path, path)
+ }
+}
+
+var _ sshfx.Packet = &StatVFSExtendedReplyPacket{}
+
+func TestStatVFSExtendedReplyPacket(t *testing.T) {
+ const (
+ id = 42
+ path = "/foo"
+ )
+
+ const (
+ BlockSize = uint64(iota + 13)
+ FragmentSize
+ Blocks
+ BlocksFree
+ BlocksAvail
+ Files
+ FilesFree
+ FilesAvail
+ FilesystemID
+ MountFlags
+ MaxNameLength
+ )
+
+ ep := &StatVFSExtendedReplyPacket{
+ BlockSize: BlockSize,
+ FragmentSize: FragmentSize,
+ Blocks: Blocks,
+ BlocksFree: BlocksFree,
+ BlocksAvail: BlocksAvail,
+ Files: Files,
+ FilesFree: FilesFree,
+ FilesAvail: FilesAvail,
+ FilesystemID: FilesystemID,
+ MountFlags: MountFlags,
+ MaxNameLength: MaxNameLength,
+ }
+
+ data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
+ if err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ want := []byte{
+ 0x00, 0x00, 0x00, 93,
+ 201,
+ 0x00, 0x00, 0x00, 42,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23,
+ }
+
+ if !bytes.Equal(data, want) {
+ t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
+ }
+
+ *ep = StatVFSExtendedReplyPacket{}
+
+ p := sshfx.ExtendedReplyPacket{
+ Data: ep,
+ }
+
+ // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
+ if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
+ t.Fatal("unexpected error:", err)
+ }
+
+ ep, ok := p.Data.(*StatVFSExtendedReplyPacket)
+ if !ok {
+ t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *StatVFSExtendedReplyPacket", p.Data)
+ }
+
+ if ep.BlockSize != BlockSize {
+ t.Errorf("UnmarshalPacketBody(): BlockSize was %d, but expected %d", ep.BlockSize, BlockSize)
+ }
+
+ if ep.FragmentSize != FragmentSize {
+ t.Errorf("UnmarshalPacketBody(): FragmentSize was %d, but expected %d", ep.FragmentSize, FragmentSize)
+ }
+
+ if ep.Blocks != Blocks {
+ t.Errorf("UnmarshalPacketBody(): Blocks was %d, but expected %d", ep.Blocks, Blocks)
+ }
+
+ if ep.BlocksFree != BlocksFree {
+ t.Errorf("UnmarshalPacketBody(): BlocksFree was %d, but expected %d", ep.BlocksFree, BlocksFree)
+ }
+
+ if ep.BlocksAvail != BlocksAvail {
+ t.Errorf("UnmarshalPacketBody(): BlocksAvail was %d, but expected %d", ep.BlocksAvail, BlocksAvail)
+ }
+
+ if ep.Files != Files {
+ t.Errorf("UnmarshalPacketBody(): Files was %d, but expected %d", ep.Files, Files)
+ }
+
+ if ep.FilesFree != FilesFree {
+ t.Errorf("UnmarshalPacketBody(): FilesFree was %d, but expected %d", ep.FilesFree, FilesFree)
+ }
+
+ if ep.FilesAvail != FilesAvail {
+ t.Errorf("UnmarshalPacketBody(): FilesAvail was %d, but expected %d", ep.FilesAvail, FilesAvail)
+ }
+
+ if ep.FilesystemID != FilesystemID {
+ t.Errorf("UnmarshalPacketBody(): FilesystemID was %d, but expected %d", ep.FilesystemID, FilesystemID)
+ }
+
+ if ep.MountFlags != MountFlags {
+ t.Errorf("UnmarshalPacketBody(): MountFlags was %d, but expected %d", ep.MountFlags, MountFlags)
+ }
+
+ if ep.MaxNameLength != MaxNameLength {
+ t.Errorf("UnmarshalPacketBody(): MaxNameLength was %d, but expected %d", ep.MaxNameLength, MaxNameLength)
+ }
+}