You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2015/07/15 10:09:52 UTC

mina-sshd git commit: [SSHD-524] Add support for "statvfs/fstatvs@openssh.com" client SFTP extensions

Repository: mina-sshd
Updated Branches:
  refs/heads/master 5da0d1a86 -> adb313ebc


[SSHD-524] Add support for "statvfs/fstatvs@openssh.com" client SFTP extensions


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/adb313eb
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/adb313eb
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/adb313eb

Branch: refs/heads/master
Commit: adb313ebc66262e47f40b045e500a5d691741b5e
Parents: 5da0d1a
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Wed Jul 15 11:09:40 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Wed Jul 15 11:09:40 2015 +0300

----------------------------------------------------------------------
 .../sshd/client/subsystem/sftp/SftpClient.java  |   4 +
 .../sshd/client/subsystem/sftp/SftpCommand.java |  42 +++-
 .../extensions/BuiltinSftpClientExtensions.java |  18 ++
 .../impl/AbstractCheckFileExtension.java        |  11 +-
 .../impl/AbstractMD5HashExtension.java          |   8 +-
 .../impl/AbstractSftpClientExtension.java       |  57 +++++
 .../openssh/OpenSSHStatExtensionInfo.java       | 160 ++++++++++++++
 .../openssh/OpenSSHStatHandleExtension.java     |  33 +++
 .../openssh/OpenSSHStatPathExtension.java       |  33 +++
 .../AbstractOpenSSHStatCommandExtension.java    |  57 +++++
 .../openssh/impl/OpenSSHFsyncExtensionImpl.java |   2 +-
 .../impl/OpenSSHStatHandleExtensionImpl.java    |  44 ++++
 .../impl/OpenSSHStatPathExtensionImpl.java      |  43 ++++
 .../apache/sshd/common/util/GenericUtils.java   |   6 +
 .../server/subsystem/sftp/SftpSubsystem.java    |  66 ++++--
 .../sshd/client/subsystem/sftp/SftpTest.java    |  12 +-
 .../sftp/extensions/OpenSSHExtensionsTest.java  |  92 --------
 .../openssh/OpenSSHExtensionsTest.java          | 217 +++++++++++++++++++
 18 files changed, 770 insertions(+), 135 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index fb73149..9a9a12b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -81,6 +81,10 @@ public interface SftpClient extends SubsystemClient {
             this.id = ValidateUtils.checkNotNullAndNotEmpty(id, "No handle ID", GenericUtils.EMPTY_OBJECT_ARRAY).clone();
         }
 
+        public int length() {
+            return id.length;
+        }
+
         /**
          * @return A <U>cloned</U> instance of the identifier in order to
          * avoid inadvertent modifications to the handle contents

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
index 64e7578..1371d7d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
@@ -23,6 +23,8 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.nio.channels.Channel;
 import java.util.Arrays;
 import java.util.Collections;
@@ -31,8 +33,11 @@ import java.util.TreeMap;
 
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.BufferUtils;
@@ -160,7 +165,7 @@ public class SftpCommand implements Channel {
                                         public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
                                             String[] comps = GenericUtils.split(args, ' ');
                                             // ignore all flag
-                                            String pathArg = GenericUtils.isEmpty(comps) ? null : comps[comps.length - 1];
+                                            String pathArg = GenericUtils.isEmpty(comps) ? null : GenericUtils.trimToEmpty(comps[comps.length - 1]);
                                             String cwd = getCurrentRemoteDirectory();
                                             if (GenericUtils.isEmpty(pathArg) || (pathArg.charAt(0) == '-')) {
                                                 pathArg = cwd;
@@ -220,8 +225,8 @@ public class SftpCommand implements Channel {
                                             String[] comps = GenericUtils.split(args, ' ');
                                             ValidateUtils.checkTrue(GenericUtils.length(comps) == 2, "Invalid number of arguments: %s", args);
 
-                                            String oldPath = resolveRemotePath(comps[0]);
-                                            String newPath = resolveRemotePath(comps[1]);
+                                            String oldPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[0]));
+                                            String newPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[1]));
                                             SftpClient sftp = getClient();
                                             sftp.rename(oldPath, newPath);
                                             return false;
@@ -230,6 +235,37 @@ public class SftpCommand implements Channel {
                                 new CommandExecutor() {
                                         @Override
                                         public String getName() {
+                                            return StatVfsExtensionParser.NAME;
+                                        }
+    
+                                        @Override
+                                        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+                                            String[] comps = GenericUtils.split(args, ' ');
+                                            ValidateUtils.checkTrue(GenericUtils.length(comps) == 1, "Invalid number of arguments: %s", args);
+
+                                            SftpClient sftp = getClient();
+                                            OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class);
+                                            ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName());
+
+                                            OpenSSHStatExtensionInfo info = ext.stat(GenericUtils.trimToEmpty(comps[0]));
+                                            Field[] fields = info.getClass().getFields();
+                                            for (Field f : fields) {
+                                                String name = f.getName();
+                                                int mod = f.getModifiers();
+                                                if (Modifier.isStatic(mod)) {
+                                                    continue;
+                                                }
+
+                                                Object value = f.get(info);
+                                                stdout.append('\t').append(name).append(": ").println(value);
+                                            }
+
+                                            return false;
+                                        }
+                                    },
+                                new CommandExecutor() {
+                                        @Override
+                                        public String getName() {
                                             return "help";
                                         }
     

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
index f1db773..a45f7b4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
@@ -33,11 +33,17 @@ import org.apache.sshd.client.subsystem.sftp.extensions.impl.CopyFileExtensionIm
 import org.apache.sshd.client.subsystem.sftp.extensions.impl.MD5FileExtensionImpl;
 import org.apache.sshd.client.subsystem.sftp.extensions.impl.MD5HandleExtensionImpl;
 import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHFsyncExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatHandleExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
 import org.apache.sshd.client.subsystem.sftp.extensions.openssh.impl.OpenSSHFsyncExtensionImpl;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.impl.OpenSSHStatHandleExtensionImpl;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.impl.OpenSSHStatPathExtensionImpl;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FstatVfsExtensionParser;
 import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -84,6 +90,18 @@ public enum BuiltinSftpClientExtensions implements SftpClientExtensionFactory {
             public OpenSSHFsyncExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
                 return new OpenSSHFsyncExtensionImpl(client, raw, extensions);
             }
+        },
+    OPENSSH_STAT_HANDLE(FstatVfsExtensionParser.NAME, OpenSSHStatHandleExtension.class) {
+            @Override   // co-variant return
+            public OpenSSHStatHandleExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new OpenSSHStatHandleExtensionImpl(client, raw, extensions);
+            }
+        },
+    OPENSSH_STAT_PATH(StatVfsExtensionParser.NAME, OpenSSHStatPathExtension.class) {
+            @Override   // co-variant return
+            public OpenSSHStatPathExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new OpenSSHStatPathExtensionImpl(client, raw, extensions);
+            }
         };
 
     private final String name;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java
index d5bb304..fa4ca35 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java
@@ -41,13 +41,8 @@ public abstract class AbstractCheckFileExtension extends AbstractSftpClientExten
     }
 
     protected Pair<String,Collection<byte[]>> doGetHash(Object target, Collection<String> algorithms, long offset, long length, int blockSize) throws IOException {
-        Buffer buffer = getCommandBuffer(Byte.MAX_VALUE);
-        String opcode = getName();
-        if (target instanceof CharSequence) {
-            buffer.putString(target.toString());
-        } else {
-            buffer.putBytes((byte[]) target);
-        }
+        Buffer buffer = getCommandBuffer(target, Byte.MAX_VALUE);
+        putTarget(buffer, target);
         buffer.putString(GenericUtils.join(algorithms, ','));
         buffer.putLong(offset);
         buffer.putLong(length);
@@ -55,7 +50,7 @@ public abstract class AbstractCheckFileExtension extends AbstractSftpClientExten
         
         if (log.isDebugEnabled()) {
             log.debug("doGetHash({})[{}] - offset={}, length={}, block-size={}",
-                      opcode, (target instanceof CharSequence) ? target : BufferUtils.printHex(BufferUtils.EMPTY_HEX_SEPARATOR, (byte[]) target),
+                      getName(), (target instanceof CharSequence) ? target : BufferUtils.printHex(BufferUtils.EMPTY_HEX_SEPARATOR, (byte[]) target),
                       Long.valueOf(offset), Long.valueOf(length), Integer.valueOf(blockSize));
         }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
index 03d5350..8c8a5d5 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
@@ -38,13 +38,9 @@ public abstract class AbstractMD5HashExtension extends AbstractSftpClientExtensi
     }
 
     protected byte[] doGetHash(Object target, long offset, long length, byte[] quickHash) throws IOException {
-        Buffer buffer = getCommandBuffer(Long.SIZE + 2 * (Long.SIZE / Byte.SIZE) + (Integer.SIZE / Byte.SIZE) + GenericUtils.length(quickHash));
+        Buffer buffer = getCommandBuffer(target, Long.SIZE + 2 * (Long.SIZE / Byte.SIZE) + (Integer.SIZE / Byte.SIZE) + GenericUtils.length(quickHash));
         String opcode = getName();
-        if (target instanceof CharSequence) {
-            buffer.putString(target.toString());
-        } else {
-            buffer.putBytes((byte[]) target);
-        }
+        putTarget(buffer, target);
         buffer.putLong(offset);
         buffer.putLong(length);
         buffer.putBytes((quickHash == null) ? GenericUtils.EMPTY_BYTE_ARRAY : quickHash);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
index 0778c2c..a7eb6d0 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
@@ -22,10 +22,12 @@ package org.apache.sshd.client.subsystem.sftp.extensions.impl;
 import java.io.IOException;
 import java.io.StreamCorruptedException;
 import java.util.Collection;
+import java.util.Map;
 
 import org.apache.sshd.client.SftpException;
 import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
@@ -48,6 +50,10 @@ public abstract class AbstractSftpClientExtension extends AbstractLoggingBean im
         this(name, client, raw, GenericUtils.isEmpty(extras) ? false : extras.contains(name));
     }
 
+    protected AbstractSftpClientExtension(String name, SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions) {
+        this(name, client, raw, GenericUtils.isEmpty(extensions) ? false : extensions.containsKey(name));
+    }
+
     protected AbstractSftpClientExtension(String name, SftpClient client, RawSftpClient raw, boolean supported) {
         this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name", GenericUtils.EMPTY_OBJECT_ARRAY);
         this.client = ValidateUtils.checkNotNull(client, "No client instance", GenericUtils.EMPTY_OBJECT_ARRAY);
@@ -99,6 +105,57 @@ public abstract class AbstractSftpClientExtension extends AbstractLoggingBean im
     }
 
     /**
+     * @param buffer The {@link Buffer}
+     * @param target A target path {@link String} or {@link Handle} or {@code byte[])
+     * to be encoded in the buffer
+     * @return The updated buffer
+     * @throws UnsupportedOperationException If target is not one of the above
+     * supported types
+     */
+    public Buffer putTarget(Buffer buffer, Object target) {
+        if (target instanceof CharSequence) {
+            buffer.putString(target.toString());
+        } else if (target instanceof byte[]) {
+            buffer.putBytes((byte[]) target);
+        } else if (target instanceof Handle) {
+            buffer.putBytes(((Handle) target).getIdentifier());
+        } else {
+            throw new UnsupportedOperationException("Unknown target type: " + target);
+        }
+
+        return buffer;
+    }
+
+    /**
+     * @param target A target path {@link String} or {@link Handle} or {@code byte[])
+     * to be encoded in the buffer
+     * @return A {@link Buffer} with the extension name set
+     * @see #getCommandBuffer(Object, int)
+     */
+    protected Buffer getCommandBuffer(Object target) {
+        return getCommandBuffer(target, 0);
+    }
+
+    /**
+     * @param target A target path {@link String} or {@link Handle} or {@code byte[])
+     * to be encoded in the buffer
+     * @param extraSize Extra size - beyond the path/handle to be allocated
+     * @return A {@link Buffer} with the extension name set
+     * @see #getCommandBuffer(int)
+     */
+    protected Buffer getCommandBuffer(Object target, int extraSize) {
+        if (target instanceof CharSequence) {
+            return getCommandBuffer((Integer.SIZE / Byte.SIZE) + ((CharSequence) target).length() + extraSize);
+        } else if (target instanceof byte[]) {
+            return getCommandBuffer((Integer.SIZE / Byte.SIZE) + ((byte[]) target).length + extraSize);
+        } else if (target instanceof Handle) {
+            return getCommandBuffer((Integer.SIZE / Byte.SIZE) + ((Handle) target).length() + extraSize);
+        } else {
+            return getCommandBuffer(extraSize);
+        }
+    }
+
+    /**
      * @param extraSize Extra size - besides the extension name
      * @return A {@link Buffer} with the extension name set
      */

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatExtensionInfo.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatExtensionInfo.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatExtensionInfo.java
new file mode 100644
index 0000000..6213505
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatExtensionInfo.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * Response for the &quot;statvfs@openssh.com&quot; and &quot;fstatvfs@openssh.com&quot;
+ * extension commands.
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/PROTOCOL?rev=1.28&content-type=text/plain">OpenSSH section 3.4</A>
+ */
+public class OpenSSHStatExtensionInfo implements Cloneable {
+    // The values of the f_flag bitmask
+    public static final long SSH_FXE_STATVFS_ST_RDONLY = 0x1; /* read-only */
+    public static final long SSH_FXE_STATVFS_ST_NOSUID = 0x2; /* no setuid */
+
+    public long f_bsize;     /* file system block size */
+    public long f_frsize;    /* fundamental fs block size */
+    public long f_blocks;    /* number of blocks (unit f_frsize) */
+    public long f_bfree;     /* free blocks in file system */
+    public long f_bavail;    /* free blocks for non-root */
+    public long f_files;     /* total file inodes */
+    public long f_ffree;     /* free file inodes */
+    public long f_favail;    /* free file inodes for to non-root */
+    public long f_fsid;      /* file system id */
+    public long f_flag;      /* bit mask of f_flag values */
+    public long f_namemax;   /* maximum filename length */
+    
+    public OpenSSHStatExtensionInfo() {
+        super();
+    }
+
+    public OpenSSHStatExtensionInfo(Buffer buffer) {
+        decode(buffer, this);
+    }
+
+    @Override
+    public int hashCode() {
+        return GenericUtils.hashCode(this.f_bsize)
+             + GenericUtils.hashCode(this.f_frsize)
+             + GenericUtils.hashCode(this.f_blocks)
+             + GenericUtils.hashCode(this.f_bfree)
+             + GenericUtils.hashCode(this.f_bavail)
+             + GenericUtils.hashCode(this.f_files)
+             + GenericUtils.hashCode(this.f_ffree)
+             + GenericUtils.hashCode(this.f_favail)
+             + GenericUtils.hashCode(this.f_fsid)
+             + GenericUtils.hashCode(this.f_flag)
+             + GenericUtils.hashCode(this.f_namemax)
+             ;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        OpenSSHStatExtensionInfo other = (OpenSSHStatExtensionInfo) obj;
+        if ((this.f_bsize == other.f_bsize)
+         && (this.f_frsize == other.f_frsize)
+         && (this.f_blocks == other.f_blocks)
+         && (this.f_bfree == other.f_bfree)
+         && (this.f_bavail == other.f_bavail)
+         && (this.f_files == other.f_files)
+         && (this.f_ffree == other.f_ffree)
+         && (this.f_favail == other.f_favail)
+         && (this.f_fsid == other.f_fsid)
+         && (this.f_flag == other.f_flag)
+         && (this.f_namemax == other.f_namemax)) {
+            return true;
+        } else {
+            return false;   // debug breakpoint
+        }
+    }
+
+    @Override
+    public OpenSSHStatExtensionInfo clone() {
+        try {
+            return getClass().cast(super.clone());
+        } catch(CloneNotSupportedException e) {
+            throw new RuntimeException("Failed to close " + toString() + ": " + e.getMessage());
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "f_bsize=" + f_bsize
+            + ",f_frsize=" + f_frsize
+            + ",f_blocks=" + f_blocks
+            + ",f_bfree=" + f_bfree
+            + ",f_bavail=" + f_bavail
+            + ",f_files=" + f_files
+            + ",f_ffree=" + f_ffree
+            + ",f_favail=" + f_favail
+            + ",f_fsid=" + f_fsid
+            + ",f_flag=0x" + Long.toHexString(f_flag)
+            + ",f_namemax=" + f_namemax
+            ;
+    }
+
+    public static void encode(Buffer buffer, OpenSSHStatExtensionInfo info) {
+        buffer.putLong(info.f_bsize);
+        buffer.putLong(info.f_frsize);
+        buffer.putLong(info.f_blocks);
+        buffer.putLong(info.f_bfree);
+        buffer.putLong(info.f_bavail);
+        buffer.putLong(info.f_files);
+        buffer.putLong(info.f_ffree);
+        buffer.putLong(info.f_favail);
+        buffer.putLong(info.f_fsid);
+        buffer.putLong(info.f_flag);
+        buffer.putLong(info.f_namemax);
+    }
+
+    public static OpenSSHStatExtensionInfo decode(Buffer buffer) {
+        OpenSSHStatExtensionInfo info = new OpenSSHStatExtensionInfo();
+        decode(buffer, info);
+        return info;
+    }
+    
+    public static void decode(Buffer buffer, OpenSSHStatExtensionInfo info) {
+        info.f_bsize = buffer.getLong();
+        info.f_frsize = buffer.getLong();
+        info.f_blocks = buffer.getLong();
+        info.f_bfree = buffer.getLong();
+        info.f_bavail = buffer.getLong();
+        info.f_files = buffer.getLong();
+        info.f_ffree = buffer.getLong();
+        info.f_favail = buffer.getLong();
+        info.f_fsid = buffer.getLong();
+        info.f_flag = buffer.getLong();
+        info.f_namemax = buffer.getLong();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatHandleExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatHandleExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatHandleExtension.java
new file mode 100644
index 0000000..e36067e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatHandleExtension.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh;
+
+import java.io.IOException;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle;
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
+
+/**
+ * Implements the &quot;fstatvfs@openssh.com&quot; extension command
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface OpenSSHStatHandleExtension extends SftpClientExtension {
+    OpenSSHStatExtensionInfo stat(Handle handle) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatPathExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatPathExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatPathExtension.java
new file mode 100644
index 0000000..a2450af
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHStatPathExtension.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh;
+
+import java.io.IOException;
+
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
+
+/**
+ * Implements the &quot;statvfs@openssh.com&quot; extension command
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/PROTOCOL?rev=1.28&content-type=text/plain">OpenSSH section 3.4</A>
+ */
+public interface OpenSSHStatPathExtension extends SftpClientExtension {
+    OpenSSHStatExtensionInfo stat(String path) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/AbstractOpenSSHStatCommandExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/AbstractOpenSSHStatCommandExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/AbstractOpenSSHStatCommandExtension.java
new file mode 100644
index 0000000..51b6817
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/AbstractOpenSSHStatCommandExtension.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh.impl;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.util.Map;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.impl.AbstractSftpClientExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractOpenSSHStatCommandExtension extends AbstractSftpClientExtension {
+    protected AbstractOpenSSHStatCommandExtension(String name, SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions) {
+        super(name, client, raw, extensions);
+    }
+    
+    protected OpenSSHStatExtensionInfo doGetStat(Object target) throws IOException {
+        Buffer buffer = getCommandBuffer(target);
+        putTarget(buffer, target);
+        
+        if (log.isDebugEnabled()) {
+            log.debug("doGetStat({})[{}]", getName(),
+                      (target instanceof CharSequence) ? target : BufferUtils.printHex(BufferUtils.EMPTY_HEX_SEPARATOR, (byte[]) target));
+        }
+
+        buffer = checkExtendedReplyBuffer(receive(sendExtendedCommand(buffer)));
+        if (buffer == null) {
+            throw new StreamCorruptedException("Missing extended reply data");
+        }
+
+        return new OpenSSHStatExtensionInfo(buffer);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHFsyncExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHFsyncExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHFsyncExtensionImpl.java
index a6cfabd..5032fc0 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHFsyncExtensionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHFsyncExtensionImpl.java
@@ -36,7 +36,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
  */
 public class OpenSSHFsyncExtensionImpl extends AbstractSftpClientExtension implements OpenSSHFsyncExtension {
     public OpenSSHFsyncExtensionImpl(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions) {
-        super(FsyncExtensionParser.NAME, client, raw, (GenericUtils.size(extensions) > 0) && extensions.containsKey(FsyncExtensionParser.NAME));
+        super(FsyncExtensionParser.NAME, client, raw, extensions);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatHandleExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatHandleExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatHandleExtensionImpl.java
new file mode 100644
index 0000000..ec0ab90
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatHandleExtensionImpl.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh.impl;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatHandleExtension;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FstatVfsExtensionParser;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class OpenSSHStatHandleExtensionImpl extends AbstractOpenSSHStatCommandExtension implements OpenSSHStatHandleExtension {
+    public OpenSSHStatHandleExtensionImpl(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions) {
+        super(FstatVfsExtensionParser.NAME, client, raw, extensions);
+    }
+
+    @Override
+    public OpenSSHStatExtensionInfo stat(Handle handle) throws IOException {
+        return doGetStat(handle.getIdentifier());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatPathExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatPathExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatPathExtensionImpl.java
new file mode 100644
index 0000000..7ed085c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHStatPathExtensionImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh.impl;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class OpenSSHStatPathExtensionImpl extends AbstractOpenSSHStatCommandExtension implements OpenSSHStatPathExtension {
+    public OpenSSHStatPathExtensionImpl(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions) {
+        super(StatVfsExtensionParser.NAME, client, raw, extensions);
+    }
+
+    @Override
+    public OpenSSHStatExtensionInfo stat(String path) throws IOException {
+        return doGetStat(path);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
index fd3cefb..568019d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
@@ -415,4 +415,10 @@ public final class GenericUtils {
         current.addSuppressed(extra);
         return current;
     }
+
+    // TODO in JDK-8 use Long.hashCode(long)
+    public static int hashCode(long value) {
+        return (int)(value ^ (value >>> 32));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
index a12811f..8f861ca 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -201,6 +201,18 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                                     new OpenSSHExtension(FsyncExtensionParser.NAME, "1")
                                 ));
 
+        public static final List<String> DEFAULT_OPEN_SSH_EXTENSIONS_NAMES =
+                Collections.unmodifiableList(new ArrayList<String>(DEFAULT_OPEN_SSH_EXTENSIONS.size()) {
+                        private static final long serialVersionUID = 1L;    // we're not serializing it
+                        
+                        {
+                            for (OpenSSHExtension ext : DEFAULT_OPEN_SSH_EXTENSIONS) {
+                                add(ext.getName());
+                            }
+                        }
+                    
+                });
+
     static {
         StringBuilder sb = new StringBuilder(2 * (1 + (HIGHER_SFTP_IMPL - LOWER_SFTP_IMPL)));
         for (int v = LOWER_SFTP_IMPL; v <= HIGHER_SFTP_IMPL; v++) {
@@ -2179,30 +2191,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
     }
 
     protected List<OpenSSHExtension> appendOpenSSHExtensions(Buffer buffer) {
-        String value = FactoryManagerUtils.getString(session, OPENSSH_EXTENSIONS_PROP);
-        List<OpenSSHExtension> extList=Collections.emptyList();
-        if (value != null) {
-            String[] pairs = GenericUtils.split(value, ',');
-            int numExts = GenericUtils.length(pairs);
-            if (numExts > 0) {
-                extList = new ArrayList<OpenSSHExtension>(numExts);
-                for (String nvp : pairs) {
-                    nvp = GenericUtils.trimToEmpty(nvp);
-                    if (GenericUtils.isEmpty(nvp)) {
-                        continue;
-                    }
-                    
-                    int pos = nvp.indexOf('=');
-                    ValidateUtils.checkTrue((pos > 0) && (pos < (nvp.length() - 1)), "Malformed OpenSSH extension spec: %s", nvp);
-                    String name = GenericUtils.trimToEmpty(nvp.substring(0, pos));
-                    String version = GenericUtils.trimToEmpty(nvp.substring(pos + 1));
-                    extList.add(new OpenSSHExtension(name, ValidateUtils.checkNotNullAndNotEmpty(version, "No version specified for OpenSSH extension %s", name)));
-                }
-            }
-        } else {
-            extList = DEFAULT_OPEN_SSH_EXTENSIONS;
-        }
-        
+        List<OpenSSHExtension> extList = resolveOpenSSHExtensions(); 
         if (GenericUtils.isEmpty(extList)) {
             return extList;
         }
@@ -2215,6 +2204,35 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         return extList;
     }
 
+    protected List<OpenSSHExtension> resolveOpenSSHExtensions() {
+        String value = FactoryManagerUtils.getString(session, OPENSSH_EXTENSIONS_PROP);
+        if (value == null) {    // No override
+            return DEFAULT_OPEN_SSH_EXTENSIONS;
+        }
+
+        String[] pairs = GenericUtils.split(value, ',');
+        int numExts = GenericUtils.length(pairs);
+        if (numExts <= 0) {     // User does not want to report ANY extensions
+            return Collections.emptyList();
+        }
+                
+        List<OpenSSHExtension> extList = new ArrayList<OpenSSHExtension>(numExts);
+        for (String nvp : pairs) {
+            nvp = GenericUtils.trimToEmpty(nvp);
+            if (GenericUtils.isEmpty(nvp)) {
+                continue;
+            }
+            
+            int pos = nvp.indexOf('=');
+            ValidateUtils.checkTrue((pos > 0) && (pos < (nvp.length() - 1)), "Malformed OpenSSH extension spec: %s", nvp);
+            String name = GenericUtils.trimToEmpty(nvp.substring(0, pos));
+            String version = GenericUtils.trimToEmpty(nvp.substring(pos + 1));
+            extList.add(new OpenSSHExtension(name, ValidateUtils.checkNotNullAndNotEmpty(version, "No version specified for OpenSSH extension %s", name)));
+        }
+        
+        return extList;
+    }
+
     protected Collection<String> getSupportedClientExtensions() {
         String value = FactoryManagerUtils.getString(session, CLIENT_EXTENSIONS_PROP);
         if (value == null) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
index d39a6f3..472d01a 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
@@ -655,10 +655,20 @@ public class SftpTest extends AbstractSftpClientTestSupport {
 
                     for (BuiltinSftpClientExtensions type : BuiltinSftpClientExtensions.VALUES) {
                         String extensionName = type.getName();
+                        boolean isOpenSSHExtension = extensionName.endsWith("@openssh.com");
                         SftpClientExtension instance = sftp.getExtension(extensionName);
+
                         assertNotNull("Extension not implemented:" + extensionName, instance);
                         assertEquals("Mismatched instance name", extensionName, instance.getName());
-                        assertTrue("Extension not supported: " + extensionName, instance.isSupported());
+
+                        if (instance.isSupported()) {
+                            if (isOpenSSHExtension) {
+                                assertTrue("Unlisted default OpenSSH extension: " + extensionName, SftpSubsystem.DEFAULT_OPEN_SSH_EXTENSIONS_NAMES.contains(extensionName));
+                            }
+                        } else {
+                            assertTrue("Unsupported non-OpenSSH extension: " + extensionName, isOpenSSHExtension);
+                            assertFalse("Unsupported default OpenSSH extension: " + extensionName, SftpSubsystem.DEFAULT_OPEN_SSH_EXTENSIONS_NAMES.contains(extensionName));
+                        }
                     }
                 }
             } finally {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/OpenSSHExtensionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/OpenSSHExtensionsTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/OpenSSHExtensionsTest.java
deleted file mode 100644
index 8b019a9..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/OpenSSHExtensionsTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.sshd.client.subsystem.sftp.extensions;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.subsystem.sftp.AbstractSftpClientTestSupport;
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
-import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHFsyncExtension;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.util.Utils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class OpenSSHExtensionsTest extends AbstractSftpClientTestSupport {
-    public OpenSSHExtensionsTest() throws IOException {
-        super();
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        setupServer();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        tearDownServer();
-    }
-
-    @Test
-    public void testFsync() throws IOException {
-        Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
-        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + ".txt");
-        byte[] expected=(getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
-
-        Path parentPath = targetPath.getParent();
-        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
-        try(SshClient client = SshClient.setUpDefaultClient()) {
-            client.start();
-            
-            try (ClientSession session = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) {
-                session.addPasswordIdentity(getCurrentTestName());
-                session.auth().verify(5L, TimeUnit.SECONDS);
-                
-                try(SftpClient sftp = session.createSftpClient()) {
-                    OpenSSHFsyncExtension fsync = assertExtensionCreated(sftp, OpenSSHFsyncExtension.class);
-                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Write, SftpClient.OpenMode.Create)) {
-                        sftp.write(fileHandle, 0L, expected);
-                        fsync.fsync(fileHandle);
-
-                        byte[] actual = Files.readAllBytes(srcFile);
-                        assertArrayEquals("Mismatched written data", expected,  actual);
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/adb313eb/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHExtensionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHExtensionsTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHExtensionsTest.java
new file mode 100644
index 0000000..f44aaf3
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHExtensionsTest.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.extensions.openssh;
+
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_EXTENDED_REPLY;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.AbstractSftpClientTestSupport;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FstatVfsExtensionParser;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystem;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class OpenSSHExtensionsTest extends AbstractSftpClientTestSupport {
+    public OpenSSHExtensionsTest() throws IOException {
+        super();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testFsync() throws IOException {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + ".txt");
+        byte[] expected=(getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+        try(SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+            
+            try (ClientSession session = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+                
+                try(SftpClient sftp = session.createSftpClient()) {
+                    OpenSSHFsyncExtension fsync = assertExtensionCreated(sftp, OpenSSHFsyncExtension.class);
+                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Write, SftpClient.OpenMode.Create)) {
+                        sftp.write(fileHandle, 0L, expected);
+                        fsync.fsync(fileHandle);
+
+                        byte[] actual = Files.readAllBytes(srcFile);
+                        assertArrayEquals("Mismatched written data", expected,  actual);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testStat() throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + ".txt");
+        Files.write(srcFile, (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8), IoUtils.EMPTY_OPEN_OPTIONS);
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+
+        final AtomicReference<String> extensionHolder = new AtomicReference<String>(null);
+        final OpenSSHStatExtensionInfo expected = new OpenSSHStatExtensionInfo();
+            {
+                expected.f_bavail = Short.MAX_VALUE;
+                expected.f_bfree = Integer.MAX_VALUE;
+                expected.f_blocks = Short.MAX_VALUE;
+                expected.f_bsize = IoUtils.DEFAULT_COPY_SIZE;
+                expected.f_favail = Long.MAX_VALUE;
+                expected.f_ffree = Byte.MAX_VALUE;
+                expected.f_files = 3777347L;
+                expected.f_flag = OpenSSHStatExtensionInfo.SSH_FXE_STATVFS_ST_RDONLY;
+                expected.f_frsize = 7365L;
+                expected.f_fsid = 1L;
+                expected.f_namemax = 256;
+            }
+        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory() {
+                @Override
+                public Command create() {
+                    return new SftpSubsystem(getExecutorService(), isShutdownOnExit(), getUnsupportedAttributePolicy()) {
+                        @Override
+                        protected List<OpenSSHExtension> resolveOpenSSHExtensions() {
+                            List<OpenSSHExtension> original = super.resolveOpenSSHExtensions();
+                            int numOriginal = GenericUtils.size(original);
+                            List<OpenSSHExtension> result = new ArrayList<OpenSSHExtension>(numOriginal + 2);
+                            if (numOriginal > 0) {
+                                result.addAll(original);
+                            }
+                            
+                            for (String name : new String[] { StatVfsExtensionParser.NAME,  FstatVfsExtensionParser.NAME}) {
+                                result.add(new OpenSSHExtension(name, "2"));
+                            }
+                            
+                            return result;
+                        }
+
+                        @Override
+                        protected void executeExtendedCommand(Buffer buffer, int id, String extension) throws IOException {
+                            if (StatVfsExtensionParser.NAME.equals(extension)
+                             || FstatVfsExtensionParser.NAME.equals(extension)) {
+                                String prev = extensionHolder.getAndSet(extension);
+                                if (prev != null) {
+                                    throw new StreamCorruptedException("executeExtendedCommand(" + extension + ") previous not null: " + prev);
+                                }
+                                
+                                buffer.clear();
+                                buffer.putByte((byte) SSH_FXP_EXTENDED_REPLY);
+                                buffer.putInt(id);
+                                OpenSSHStatExtensionInfo.encode(buffer, expected);
+                                send(buffer);
+                            } else {
+                                super.executeExtendedCommand(buffer, id, extension);
+                            }
+                        }
+                    };
+                }
+            }));
+        
+        try(SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+            
+            try (ClientSession session = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+                
+                try(SftpClient sftp = session.createSftpClient()) {
+                    {
+                        OpenSSHStatPathExtension pathStat = assertExtensionCreated(sftp, OpenSSHStatPathExtension.class);
+                        OpenSSHStatExtensionInfo actual = pathStat.stat(srcPath);
+                        String invokedExtension = extensionHolder.getAndSet(null);
+                        assertEquals("Mismatched invoked extension", pathStat.getName(), invokedExtension);
+                        assertOpenSSHStatExtensionInfoEquals(invokedExtension, expected, actual);
+                    }
+                    
+                    try(CloseableHandle handle = sftp.open(srcPath)) {
+                        OpenSSHStatHandleExtension handleStat = assertExtensionCreated(sftp, OpenSSHStatHandleExtension.class);
+                        OpenSSHStatExtensionInfo actual = handleStat.stat(handle);
+                        String invokedExtension = extensionHolder.getAndSet(null);
+                        assertEquals("Mismatched invoked extension", handleStat.getName(), invokedExtension);
+                        assertOpenSSHStatExtensionInfoEquals(invokedExtension, expected, actual);
+                    }
+                }
+            }
+        }
+    }
+    
+    private static void assertOpenSSHStatExtensionInfoEquals(String extension, OpenSSHStatExtensionInfo expected, OpenSSHStatExtensionInfo actual) throws Exception {
+        Field[] fields = expected.getClass().getFields();
+        for (Field f : fields) {
+            String name = f.getName();
+            int mod = f.getModifiers();
+            if (Modifier.isStatic(mod)) {
+                continue;
+            }
+            Object expValue = f.get(expected), actValue = f.get(actual);
+            assertEquals(extension + "[" + name + "]", expValue, actValue);
+        }
+    }
+}