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 13:52:17 UTC

[1/2] mina-sshd git commit: [SSHD-516] Add support for "space-available" extension

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


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyDataExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyDataExtensionImplTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyDataExtensionImplTest.java
new file mode 100644
index 0000000..b9597d7
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyDataExtensionImplTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+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.CopyDataExtension;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.io.IoUtils;
+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.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class CopyDataExtensionImplTest extends AbstractSftpClientTestSupport {
+    private static final List<Object[]> PARAMETERS =
+            Collections.unmodifiableList(
+                    Arrays.<Object[]>asList(
+                                new Object[] {
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                                        Integer.valueOf(0),
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                                        Long.valueOf(0L)
+                                    },
+                                new Object[] {
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 2),
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 4),
+                                        Long.valueOf(0L)
+                                    },
+                                new Object[] {
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 2),
+                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 4),
+                                        Long.valueOf(IoUtils.DEFAULT_COPY_SIZE / 2)
+                                    },
+                                new Object[] {
+                                        Integer.valueOf(Byte.MAX_VALUE),
+                                        Integer.valueOf(Byte.MAX_VALUE / 2),
+                                        Integer.valueOf(Byte.MAX_VALUE),    // attempt to read more than available
+                                        Long.valueOf(0L)
+                                    }
+                            ));
+
+    @Parameters(name = "size={0}, readOffset={1}, readLength={2}, writeOffset={3}")
+    public static Collection<Object[]> parameters() {
+        return PARAMETERS;
+    }
+
+    private int size, srcOffset, length;
+    private long dstOffset;
+
+    public CopyDataExtensionImplTest(int size, int srcOffset, int length, long dstOffset) throws IOException {
+        this.size = size;
+        this.srcOffset = srcOffset;
+        this.length = length;
+        this.dstOffset = dstOffset;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testCopyDataExtension() throws Exception {
+        testCopyDataExtension(size, srcOffset, length, dstOffset);
+    }
+
+    private void testCopyDataExtension(int dataSize, int readOffset, int readLength, long writeOffset) throws Exception {
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName()
+                     + "-" + dataSize
+                     + "-" + readOffset + "/" + readLength + "/" + writeOffset
+                     + System.getProperty("line.separator"))
+                .getBytes(StandardCharsets.UTF_8)
+                ;
+        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(dataSize + seed.length)) {
+            while (baos.size() < dataSize) {
+                baos.write(seed);
+            }
+            
+            testCopyDataExtension(baos.toByteArray(), readOffset, readLength, writeOffset);
+        }
+    }
+
+    private void testCopyDataExtension(byte[] data, int readOffset, int readLength, long writeOffset) throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path parentPath = targetPath.getParent();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        LinkOption[] options = IoUtils.getLinkOptions(false);
+        String baseName = readOffset + "-" + readLength + "-" + writeOffset;
+        Path srcFile = assertHierarchyTargetFolderExists(lclSftp, options).resolve(baseName + "-src.txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+
+        Path dstFile = srcFile.getParent().resolve(baseName + "-dst.txt");
+        if (Files.exists(dstFile, options)) {
+            Files.delete(dstFile);
+        }
+        String dstPath = Utils.resolveRelativeRemotePath(parentPath, dstFile);
+
+        try(SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+
+            if (writeOffset > 0L) {
+                Factory<? extends Random> factory = client.getRandomFactory();
+                Random randomizer = factory.create();
+                long totalLength = writeOffset + readLength;
+                byte[] workBuf = new byte[(int) Math.min(totalLength, IoUtils.DEFAULT_COPY_SIZE)];
+                try(OutputStream output = Files.newOutputStream(dstFile, IoUtils.EMPTY_OPEN_OPTIONS)) {
+                    while(totalLength > 0L) {
+                        randomizer.fill(workBuf);
+                        output.write(workBuf);
+                        totalLength -= workBuf.length;
+                    }
+                }
+            }
+
+            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()) {
+                    CopyDataExtension ext = assertExtensionCreated(sftp, CopyDataExtension.class);
+                    try(CloseableHandle readHandle = sftp.open(srcPath, SftpClient.OpenMode.Read);
+                        CloseableHandle writeHandle = sftp.open(dstPath, SftpClient.OpenMode.Write, SftpClient.OpenMode.Create)) {
+                        ext.copyData(readHandle, readOffset, readLength, writeHandle, writeOffset);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+        
+        int available = data.length, required = readOffset + readLength;
+        if (required > available) {
+            required = available; 
+        }
+        byte[] expected = new byte[required - readOffset];
+        System.arraycopy(data, readOffset, expected, 0, expected.length);
+        
+        byte[] actual = new byte[expected.length];
+        try(FileChannel channel = FileChannel.open(dstFile, IoUtils.EMPTY_OPEN_OPTIONS)) {
+            int readLen = channel.read(ByteBuffer.wrap(actual), writeOffset);
+            assertEquals("Mismatched read data size", expected.length, readLen);
+        }
+        assertArrayEquals("Mismatched copy data", expected, actual);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImplTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImplTest.java
new file mode 100644
index 0000000..7e5c87d
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SftpException;
+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.extensions.CopyFileExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.io.IoUtils;
+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 CopyFileExtensionImplTest extends AbstractSftpClientTestSupport {
+    public CopyFileExtensionImplTest() throws IOException {
+        super();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testCopyFileExtension() throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(lclSftp);
+
+        byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve("src.txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+        Path dstFile = lclSftp.resolve("dst.txt");
+        String dstPath = Utils.resolveRelativeRemotePath(parentPath, dstFile);
+        
+        LinkOption[] options = IoUtils.getLinkOptions(false);
+        assertFalse("Destination file unexpectedly exists", Files.exists(dstFile, options));
+
+        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()) {
+                    CopyFileExtension ext = assertExtensionCreated(sftp, CopyFileExtension.class);
+                    ext.copyFile(srcPath, dstPath, false);
+                    assertTrue("Source file not preserved", Files.exists(srcFile, options));
+                    assertTrue("Destination file not created", Files.exists(dstFile, options));
+                    
+                    byte[] actual = Files.readAllBytes(dstFile);
+                    assertArrayEquals("Mismatched copied data", data, actual);
+                    
+                    try {
+                        ext.copyFile(srcPath, dstPath, false);
+                        fail("Unexpected success to overwrite existing destination: " + dstFile);
+                    } catch(IOException e) {
+                        assertTrue("Not an SftpException", e instanceof SftpException);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImplTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImplTest.java
new file mode 100644
index 0000000..08c561d
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImplTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.nio.file.FileStore;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+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.extensions.SpaceAvailableExtension;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo;
+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 SpaceAvailableExtensionImplTest extends AbstractSftpClientTestSupport {
+    public SpaceAvailableExtensionImplTest() throws IOException {
+        super();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testFileStoreReport() throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Path parentPath = targetPath.getParent();
+        FileStore store = Files.getFileStore(lclSftp.getRoot());
+        final String queryPath = Utils.resolveRelativeRemotePath(parentPath, lclSftp);
+        final SpaceAvailableExtensionInfo expected = new SpaceAvailableExtensionInfo(store);
+        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory() {
+                @Override
+                public Command create() {
+                    return new SftpSubsystem(getExecutorService(), isShutdownOnExit(), getUnsupportedAttributePolicy()) {
+                        @Override
+                        protected SpaceAvailableExtensionInfo doSpaceAvailable(int id, String path) throws IOException {
+                            if (!queryPath.equals(path)) {
+                                throw new StreamCorruptedException("Mismatched query paths: expected=" + queryPath + ", actual=" + path);
+                            }
+
+                            return expected;
+                        }
+                    };
+                }
+            }));
+
+        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()) {
+                    SpaceAvailableExtension ext = assertExtensionCreated(sftp, SpaceAvailableExtension.class);
+                    SpaceAvailableExtensionInfo actual = ext.available(queryPath);
+                    assertEquals("Mismatched information", expected, actual);
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/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
deleted file mode 100644
index f44aaf3..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/OpenSSHExtensionsTest.java
+++ /dev/null
@@ -1,217 +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.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);
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHExtensionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHExtensionsTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHExtensionsTest.java
new file mode 100644
index 0000000..b6910af
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/impl/OpenSSHExtensionsTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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 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.client.subsystem.sftp.extensions.openssh.OpenSSHFsyncExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatHandleExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
+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);
+        }
+    }
+}


[2/2] mina-sshd git commit: [SSHD-516] Add support for "space-available" extension

Posted by lg...@apache.org.
[SSHD-516] Add support for "space-available" extension


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

Branch: refs/heads/master
Commit: ea46a25d2817c1e68cf65e5d1b6e6bd82fd8b820
Parents: adb313e
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Wed Jul 15 14:52:02 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Wed Jul 15 14:52:02 2015 +0300

----------------------------------------------------------------------
 .../extensions/BuiltinSftpClientExtensions.java |   7 +
 .../extensions/SpaceAvailableExtension.java     |  33 +++
 .../impl/SpaceAvailableExtensionImpl.java       |  53 +++++
 .../sshd/common/file/root/RootedFileSystem.java |   4 +
 .../file/root/RootedFileSystemProvider.java     |  99 ++++----
 .../common/subsystem/sftp/SftpConstants.java    |   1 +
 .../extensions/SpaceAvailableExtensionInfo.java | 130 +++++++++++
 .../server/subsystem/sftp/SftpSubsystem.java    |  44 +++-
 .../sftp/AbstractSftpClientTestSupport.java     |   5 +-
 .../sshd/client/subsystem/sftp/SftpTest.java    |  50 ----
 .../AbstractCheckFileExtensionTest.java         | 231 ------------------
 .../AbstractMD5HashExtensionTest.java           | 178 --------------
 .../extensions/CopyDataExtensionImplTest.java   | 200 ----------------
 .../impl/AbstractCheckFileExtensionTest.java    | 233 +++++++++++++++++++
 .../impl/AbstractMD5HashExtensionTest.java      | 180 ++++++++++++++
 .../impl/CopyDataExtensionImplTest.java         | 201 ++++++++++++++++
 .../impl/CopyFileExtensionImplTest.java         | 109 +++++++++
 .../impl/SpaceAvailableExtensionImplTest.java   | 108 +++++++++
 .../openssh/OpenSSHExtensionsTest.java          | 217 -----------------
 .../openssh/impl/OpenSSHExtensionsTest.java     | 221 ++++++++++++++++++
 20 files changed, 1372 insertions(+), 932 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/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 a45f7b4..21da24c 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
@@ -32,6 +32,7 @@ import org.apache.sshd.client.subsystem.sftp.extensions.impl.CopyDataExtensionIm
 import org.apache.sshd.client.subsystem.sftp.extensions.impl.CopyFileExtensionImpl;
 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.impl.SpaceAvailableExtensionImpl;
 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;
@@ -85,6 +86,12 @@ public enum BuiltinSftpClientExtensions implements SftpClientExtensionFactory {
                 return new CheckFileHandleExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
             }
         },
+    SPACE_AVAILABLE(SftpConstants.EXT_SPACE_AVAILABLE, SpaceAvailableExtension.class) {
+            @Override   // co-variant return
+            public SpaceAvailableExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new SpaceAvailableExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
+            }
+        },
     OPENSSH_FSYNC(FsyncExtensionParser.NAME, OpenSSHFsyncExtension.class) {
             @Override   // co-variant return
             public OpenSSHFsyncExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SpaceAvailableExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SpaceAvailableExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SpaceAvailableExtension.java
new file mode 100644
index 0000000..bec900e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SpaceAvailableExtension.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;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo;
+
+/**
+ * Implements the &quot;space-availble&quot; extension
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+     * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 section 9.2</A>
+ */
+public interface SpaceAvailableExtension extends SftpClientExtension {
+    SpaceAvailableExtensionInfo available(String path) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImpl.java
new file mode 100644
index 0000000..17cc627
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/SpaceAvailableExtensionImpl.java
@@ -0,0 +1,53 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.SpaceAvailableExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SpaceAvailableExtensionImpl extends AbstractSftpClientExtension implements SpaceAvailableExtension {
+    public SpaceAvailableExtensionImpl(SftpClient client, RawSftpClient raw, Collection<String> extra) {
+        super(SftpConstants.EXT_SPACE_AVAILABLE, client, raw, extra);
+    }
+
+    @Override
+    public SpaceAvailableExtensionInfo available(String path) throws IOException {
+        Buffer buffer = getCommandBuffer(path);
+        buffer.putString(path);
+        buffer = checkExtendedReplyBuffer(receive(sendExtendedCommand(buffer)));
+
+        if (buffer == null) {
+            throw new StreamCorruptedException("Missing extended reply data");
+        }
+        
+        return new SpaceAvailableExtensionInfo(buffer);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java
index 0140cfe..a9f5529 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java
@@ -43,6 +43,10 @@ public class RootedFileSystem extends BaseFileSystem<RootedPath> {
         this.rootFs = root.getFileSystem();
     }
 
+    public FileSystem getRootFileSystem() {
+        return rootFs;
+    }
+
     public Path getRoot() {
         return rootPath;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java
index 2869417..c835868 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java
@@ -34,6 +34,7 @@ import java.nio.file.FileSystem;
 import java.nio.file.FileSystemAlreadyExistsException;
 import java.nio.file.FileSystemException;
 import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
@@ -72,46 +73,37 @@ public class RootedFileSystemProvider extends FileSystemProvider {
 
     @Override
     public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
-        Path path = uriToPath(uri);
-        Path localPath2 = ensureDirectory(path).toRealPath();
+        return newFileSystem(uri, uriToPath(uri), env);
+    }
+
+    @Override
+    public FileSystem getFileSystem(URI uri) {
+        return getFileSystem(uriToPath(uri));
+    }
+
+    @Override
+    public FileSystem newFileSystem(Path path, Map<String, ?> env) throws IOException {
+        return newFileSystem(path, path, env);
+    }
 
+    protected FileSystem newFileSystem(Object src, Path path, Map<String, ?> env) throws IOException {
+        Path root = ensureDirectory(path).toRealPath();
         RootedFileSystem rootedFs=null;
         synchronized (fileSystems) {
-            if (!this.fileSystems.containsKey(localPath2)) {
+            if (!this.fileSystems.containsKey(root)) {
                 rootedFs = new RootedFileSystem(this, path, env);
-                this.fileSystems.put(localPath2, rootedFs);
+                this.fileSystems.put(root, rootedFs);
             }
         }
 
         // do all the throwing outside the synchronized block to minimize its lock time
         if (rootedFs == null) {
-            throw new FileSystemAlreadyExistsException("newFileSystem(" + uri + ") already mapped " + localPath2);
+            throw new FileSystemAlreadyExistsException("newFileSystem(" + src + ") already mapped " + root);
         }
 
         return rootedFs;
     }
 
-    @Override
-    public FileSystem getFileSystem(URI uri) {
-        try {
-            FileSystem fileSystem = getFileSystem(uriToPath(uri));
-            if (fileSystem == null) {
-                throw new FileSystemNotFoundException(uri.toString());
-            }
-
-            return fileSystem;
-        } catch(IOException e) {
-            FileSystemNotFoundException err = new FileSystemNotFoundException(uri.toString());
-            err.initCause(e);
-            throw err;
-        }
-    }
-
-    @Override
-    public FileSystem newFileSystem(Path path, Map<String, ?> env) throws IOException {
-        return new RootedFileSystem(this, ensureDirectory(path), env);
-    }
-
     protected Path uriToPath(URI uri) {
         String scheme = uri.getScheme(), expected = getScheme();
         if ((scheme == null) || (!scheme.equalsIgnoreCase(expected))) {
@@ -232,7 +224,6 @@ public class RootedFileSystemProvider extends FileSystemProvider {
         Path r = unroot(link);
         FileSystemProvider p = provider(r);
         return root(link.getFileSystem(), p.readSymbolicLink(r));
-
     }
 
     @Override
@@ -268,34 +259,40 @@ public class RootedFileSystemProvider extends FileSystemProvider {
 
     @Override
     public FileStore getFileStore(Path path) throws IOException {
-        FileSystem fileSystem = getFileSystem(path);
-        if (fileSystem == null) {
-            throw new FileSystemNotFoundException(path.toString());
-        }
-
-        Iterable<FileStore> stores = fileSystem.getFileStores();
-        if (stores == null) {
-            throw new FileSystemException(path.toString(), path.toString(), "No stores");
-        }
-        
-        for (FileStore s : stores) {
-            return s;
-        }
-
-        throw new FileSystemException(path.toString(), path.toString(), "empty stores");
+        RootedFileSystem fileSystem = getFileSystem(path);
+        Path root = fileSystem.getRoot();
+        return Files.getFileStore(root);
     }
 
-    protected RootedFileSystem getFileSystem(Path path) throws IOException {
-        try {
-            Path real = path.toRealPath();
-            synchronized (fileSystems) {
-                return fileSystems.get(real);
+    protected RootedFileSystem getFileSystem(Path path) throws FileSystemNotFoundException {
+        Path real = unroot(path, false);
+        Path rootInstance = null;
+        RootedFileSystem fsInstance = null;
+        synchronized (fileSystems) {
+            for (Map.Entry<Path,RootedFileSystem> fse : fileSystems.entrySet()) {
+                Path root = fse.getKey();
+                RootedFileSystem fs = fse.getValue();
+                if (real.equals(root)) {
+                    return fs;  // we were lucky to have the root
+                }
+                
+                if (!real.startsWith(root)) {
+                    continue;
+                }
+                
+                // if already have a candidate prefer the longer match since both are prefixes of the real path
+                if ((rootInstance == null) || (rootInstance.getNameCount() < root.getNameCount())) {
+                    rootInstance = root;
+                    fsInstance = fs;
+                }
             }
-        } catch (IOException e) {
-            FileSystemNotFoundException err = new FileSystemNotFoundException(path.toString());
-            err.initCause(e);
-            throw err;
         }
+
+        if (fsInstance == null) {
+            throw new FileSystemNotFoundException(path.toString());
+        }
+        
+        return fsInstance;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
index e719de5..44abceb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
@@ -237,6 +237,7 @@ public final class SftpConstants {
         public static final int MIN_CHKFILE_BLOCKSIZE = 256;
         public static final String EXT_CHKFILE_RESPONSE = "check-file";
     public static final String EXT_COPYDATA = "copy-data";
+    public static final String EXT_SPACE_AVAILABLE = "space-available";
 
     private SftpConstants() {
         throw new UnsupportedOperationException("No instance");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/SpaceAvailableExtensionInfo.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/SpaceAvailableExtensionInfo.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/SpaceAvailableExtensionInfo.java
new file mode 100644
index 0000000..003116c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/SpaceAvailableExtensionInfo.java
@@ -0,0 +1,130 @@
+/*
+ * 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.common.subsystem.sftp.extensions;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 section 9.2</A>
+ */
+public class SpaceAvailableExtensionInfo implements Cloneable {
+    public long bytesOnDevice;
+    public long unusedBytesOnDevice;
+    public long bytesAvailableToUser;
+    public long unusedBytesAvailableToUser;
+    public int bytesPerAllocationUnit;
+
+    public SpaceAvailableExtensionInfo() {
+        super();
+    }
+
+    public SpaceAvailableExtensionInfo(Buffer buffer) {
+        decode(buffer, this);
+    }
+
+    public SpaceAvailableExtensionInfo(FileStore store) throws IOException {
+        bytesOnDevice = store.getTotalSpace();
+        
+        long unallocated = store.getUnallocatedSpace(), usable = store.getUsableSpace();
+        unusedBytesOnDevice = Math.max(unallocated, usable);
+        
+        // the rest are intentionally  left zero indicating "UNKNOWN"
+    }
+
+    @Override
+    public int hashCode() {
+        return GenericUtils.hashCode(bytesOnDevice)
+             + GenericUtils.hashCode(unusedBytesOnDevice)
+             + GenericUtils.hashCode(bytesAvailableToUser)
+             + GenericUtils.hashCode(unusedBytesAvailableToUser)
+             + bytesPerAllocationUnit
+             ;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        SpaceAvailableExtensionInfo other = (SpaceAvailableExtensionInfo) obj;
+        if ((this.bytesOnDevice == other.bytesOnDevice)
+         && (this.unusedBytesOnDevice == other.unusedBytesOnDevice)
+         && (this.bytesAvailableToUser == other.bytesAvailableToUser)
+         && (this.unusedBytesAvailableToUser == other.unusedBytesAvailableToUser)
+         && (this.bytesPerAllocationUnit == other.bytesPerAllocationUnit)) {
+            return true;
+        } else {
+            return false;   // debug breakpoint
+        }
+    }
+
+    @Override
+    public SpaceAvailableExtensionInfo clone() {
+        try {
+            return getClass().cast(super.clone());
+        } catch(CloneNotSupportedException e) {
+            throw new RuntimeException("Failed to close " + toString() + ": " + e.getMessage());
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "bytesOnDevice=" + bytesOnDevice
+            + ",unusedBytesOnDevice=" + unusedBytesOnDevice
+            + ",bytesAvailableToUser=" + bytesAvailableToUser
+            + ",unusedBytesAvailableToUser=" + unusedBytesAvailableToUser
+            + ",bytesPerAllocationUnit=" + bytesPerAllocationUnit
+            ;
+    }
+    
+    public static SpaceAvailableExtensionInfo decode(Buffer buffer) {
+        SpaceAvailableExtensionInfo info = new SpaceAvailableExtensionInfo();
+        decode(buffer, info);
+        return info;
+    }
+
+    public static void decode(Buffer buffer, SpaceAvailableExtensionInfo info) {
+        info.bytesOnDevice = buffer.getLong();
+        info.unusedBytesOnDevice = buffer.getLong();
+        info.bytesAvailableToUser = buffer.getLong();
+        info.unusedBytesAvailableToUser = buffer.getLong();
+        info.bytesPerAllocationUnit = buffer.getInt();
+    }
+    
+    public static void encode(Buffer buffer, SpaceAvailableExtensionInfo info) {
+        buffer.putLong(info.bytesOnDevice);
+        buffer.putLong(info.unusedBytesOnDevice);
+        buffer.putLong(info.bytesAvailableToUser);
+        buffer.putLong(info.unusedBytesAvailableToUser);
+        buffer.putInt(info.bytesPerAllocationUnit & 0xFFFFFFFFL);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/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 8f861ca..f840709 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
@@ -36,6 +36,7 @@ import java.nio.file.CopyOption;
 import java.nio.file.DirectoryNotEmptyException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileStore;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystemLoopException;
 import java.nio.file.FileSystems;
@@ -89,6 +90,7 @@ import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.file.FileSystemAware;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo;
 import org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension;
 import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser;
 import org.apache.sshd.common.util.GenericUtils;
@@ -173,9 +175,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
          */
         public static final Set<String> DEFAULT_SUPPORTED_CLIENT_EXTENSIONS =
                 // TODO text-seek - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt
-                // TODO space-available - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
                 // TODO home-directory - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
-                // TODO check-file-handle/check-file-name - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.2
                 Collections.unmodifiableSet(
                         GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER,
                                 Arrays.asList(
@@ -185,7 +185,8 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                                         SftpConstants.EXT_MD5HASH_HANDLE,
                                         SftpConstants.EXT_CHKFILE_HANDLE,
                                         SftpConstants.EXT_CHKFILE_NAME,
-                                        SftpConstants.EXT_COPYDATA
+                                        SftpConstants.EXT_COPYDATA,
+                                        SftpConstants.EXT_SPACE_AVAILABLE
                                 )));
 
     /**
@@ -744,6 +745,9 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             case FsyncExtensionParser.NAME:
                 doOpenSSHFsync(buffer, id);
                 break;
+            case SftpConstants.EXT_SPACE_AVAILABLE:
+                doSpaceAvailable(buffer, id);
+                break;
             default:
                 log.info("Received unsupported SSH_FXP_EXTENDED({})", extension);
                 sendStatus(BufferUtils.clear(buffer), id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented");
@@ -751,6 +755,40 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         }
     }
 
+    protected void doSpaceAvailable(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        SpaceAvailableExtensionInfo info;
+        try {
+             info = doSpaceAvailable(id, path);
+        } catch(IOException | RuntimeException e) {
+            sendStatus(BufferUtils.clear(buffer), id, e);
+            return;
+        }
+
+        buffer.clear();
+        buffer.putByte((byte) SSH_FXP_EXTENDED_REPLY);
+        buffer.putInt(id);
+        SpaceAvailableExtensionInfo.encode(buffer, info);
+        send(buffer);
+    }
+
+    protected SpaceAvailableExtensionInfo doSpaceAvailable(int id, String path) throws IOException {
+        Path file = resolveFile(path);
+        Path abs = file.toAbsolutePath();
+        Path nrm = abs.normalize();
+        if (log.isDebugEnabled()) {
+            log.debug("doSpaceAvailable(id={}) path={}[{}]", Integer.valueOf(id), path, nrm);
+        }
+
+        FileStore store = Files.getFileStore(nrm);
+        if (log.isTraceEnabled()) {
+            log.trace("doSpaceAvailable(id={}) path={}[{}] - {}[{}]",
+                      Integer.valueOf(id), path, nrm, store.name(), store.type());
+        }
+        
+        return new SpaceAvailableExtensionInfo(store);
+    }
+
     protected void doTextSeek(Buffer buffer, int id) throws IOException {
         String handle = buffer.getString();
         long line = buffer.getLong();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
index d4b7294..cb820d4 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
@@ -45,16 +45,17 @@ import org.apache.sshd.util.Utils;
 public abstract class AbstractSftpClientTestSupport extends BaseTestSupport {
     protected SshServer sshd;
     protected int port;
+    protected final FileSystem rootFileSystem;
     protected final FileSystemFactory fileSystemFactory;
 
     protected AbstractSftpClientTestSupport() throws IOException {
         Path targetPath = detectTargetFolder().toPath();
         Path parentPath = targetPath.getParent();
-        final FileSystem fileSystem = new RootedFileSystemProvider().newFileSystem(parentPath, Collections.<String,Object>emptyMap());
+        rootFileSystem = new RootedFileSystemProvider().newFileSystem(parentPath, Collections.<String,Object>emptyMap());
         fileSystemFactory = new FileSystemFactory() {
             @Override
             public FileSystem createFileSystem(Session session) throws IOException {
-                return fileSystem;
+                return rootFileSystem;
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/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 472d01a..f6e85cf 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
@@ -32,7 +32,6 @@ import java.io.OutputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
-import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.EnumSet;
@@ -44,11 +43,9 @@ import java.util.Vector;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import org.apache.sshd.client.SftpException;
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
-import org.apache.sshd.client.subsystem.sftp.extensions.CopyFileExtension;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.FactoryManager;
@@ -567,53 +564,6 @@ public class SftpTest extends AbstractSftpClientTestSupport {
     }
 
     @Test
-    public void testCopyFileExtension() throws Exception {
-        Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(lclSftp);
-
-        byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
-        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve("src.txt");
-        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
-
-        Path parentPath = targetPath.getParent();
-        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
-        Path dstFile = lclSftp.resolve("dst.txt");
-        String dstPath = Utils.resolveRelativeRemotePath(parentPath, dstFile);
-        
-        LinkOption[] options = IoUtils.getLinkOptions(false);
-        assertFalse("Destination file unexpectedly exists", Files.exists(dstFile, options));
-
-        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()) {
-                    CopyFileExtension ext = assertExtensionCreated(sftp, CopyFileExtension.class);
-                    ext.copyFile(srcPath, dstPath, false);
-                    assertTrue("Source file not preserved", Files.exists(srcFile, options));
-                    assertTrue("Destination file not created", Files.exists(dstFile, options));
-                    
-                    byte[] actual = Files.readAllBytes(dstFile);
-                    assertArrayEquals("Mismatched copied data", data, actual);
-                    
-                    try {
-                        ext.copyFile(srcPath, dstPath, false);
-                        fail("Unexpected success to overwrite existing destination: " + dstFile);
-                    } catch(IOException e) {
-                        assertTrue("Not an SftpException", e instanceof SftpException);
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test
     public void testServerExtensionsDeclarations() throws Exception {
         try(SshClient client = SshClient.setUpDefaultClient()) {
             client.start();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java
deleted file mode 100644
index 6a075a7..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java
+++ /dev/null
@@ -1,231 +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.ByteArrayOutputStream;
-import java.io.IOException;
-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.Collection;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SftpException;
-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.NamedResource;
-import org.apache.sshd.common.digest.BuiltinDigests;
-import org.apache.sshd.common.digest.Digest;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.Pair;
-import org.apache.sshd.common.util.buffer.BufferUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-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.runner.RunWith;
-import org.junit.runners.MethodSorters;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
-public class AbstractCheckFileExtensionTest extends AbstractSftpClientTestSupport {
-    private static final Collection<Integer> DATA_SIZES =
-            Collections.unmodifiableList(
-                    Arrays.asList(
-                            Integer.valueOf(Byte.MAX_VALUE),
-                            Integer.valueOf(SftpConstants.MIN_CHKFILE_BLOCKSIZE),
-                            Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
-                            Integer.valueOf(Byte.SIZE *IoUtils.DEFAULT_COPY_SIZE)
-                    ));
-    private static final Collection<Integer> BLOCK_SIZES =
-            Collections.unmodifiableList(
-                    Arrays.asList(
-                            Integer.valueOf(0),
-                            Integer.valueOf(SftpConstants.MIN_CHKFILE_BLOCKSIZE),
-                            Integer.valueOf(1024),
-                            Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE)
-                        ));
-    @SuppressWarnings("synthetic-access")
-    private static final Collection<Object[]> PARAMETERS =
-            Collections.unmodifiableCollection(new LinkedList<Object[]>() {
-                private static final long serialVersionUID = 1L;    // we're not serializing it
-                
-                {
-                    for (NamedFactory<?> factory : BuiltinDigests.VALUES) {
-                        String algorithm = factory.getName();
-                        for (Number dataSize : DATA_SIZES) {
-                            for (Number blockSize : BLOCK_SIZES) {
-                                add(new Object[] { algorithm, dataSize, blockSize });
-                            }
-                        }
-                    }
-                }
-            });
-
-    @Parameters(name = "{0} - dataSize={1}, blockSize={2}")
-    public static Collection<Object[]> parameters() {
-        return PARAMETERS;
-    }
-
-    private final String algorithm;
-    private final int dataSize, blockSize;
-
-    public AbstractCheckFileExtensionTest(String algorithm, int dataSize, int blockSize) throws IOException {
-        this.algorithm = algorithm;
-        this.dataSize = dataSize;
-        this.blockSize = blockSize;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        setupServer();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        tearDownServer();
-    }
-
-    @Test
-    public void testCheckFileExtension() throws Exception {
-        testCheckFileExtension(algorithm, dataSize, blockSize);
-    }
-
-    private void testCheckFileExtension(String expectedAlgorithm, int inputDataSize, int hashBlockSize) throws Exception {
-        NamedFactory<? extends Digest> factory = BuiltinDigests.fromFactoryName(expectedAlgorithm);
-        Digest digest = null;
-        if (blockSize == 0) {
-            digest = factory.create();
-            digest.init();
-        }
-
-        byte[] seed = (getClass().getName() + "#" + getCurrentTestName()
-                     + "-" + expectedAlgorithm
-                     + "-" + inputDataSize + "/" + hashBlockSize
-                     + System.getProperty("line.separator"))
-                .getBytes(StandardCharsets.UTF_8)
-                ;
-
-        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(inputDataSize + seed.length)) {
-            while (baos.size() < inputDataSize) {
-                baos.write(seed);
-                
-                if (digest != null) {
-                    digest.update(seed);
-                }
-            }
-
-            testCheckFileExtension(factory, baos.toByteArray(), hashBlockSize, (digest == null) ? null : digest.digest());
-        }
-    }
-
-    private void testCheckFileExtension(NamedFactory<? extends Digest> factory, byte[] data, int hashBlockSize, byte[] expectedHash) throws Exception {
-        Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
-        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve(factory.getName() + "-data-" + data.length + "-" + hashBlockSize + ".txt");
-        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
-
-        List<String> algorithms = new ArrayList<String>(BuiltinDigests.VALUES.size());
-        // put the selected algorithm 1st and then the rest
-        algorithms.add(factory.getName());
-        for (NamedFactory<? extends Digest> f : BuiltinDigests.VALUES) {
-            if (f == factory) {
-                continue;
-            }
-            
-            algorithms.add(f.getName());
-        }
-
-        Path parentPath = targetPath.getParent();
-        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
-        String srcFolder = Utils.resolveRelativeRemotePath(parentPath, srcFile.getParent());
-
-        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()) {
-                    CheckFileNameExtension file = assertExtensionCreated(sftp, CheckFileNameExtension.class);
-                    try {
-                        Pair<String,?> result = file.checkFileName(srcFolder, algorithms, 0L, 0L, hashBlockSize);
-                        fail("Unexpected success to hash folder=" + srcFolder + ": " + result.getFirst());
-                    } catch(IOException e) {    // expected - not allowed to hash a folder
-                        assertTrue("Not an SftpException", e instanceof SftpException);
-                    }
-
-                    CheckFileHandleExtension hndl = assertExtensionCreated(sftp, CheckFileHandleExtension.class);
-                    try(CloseableHandle dirHandle = sftp.openDir(srcFolder)) {
-                        try {
-                            Pair<String,?> result = hndl.checkFileHandle(dirHandle, algorithms, 0L, 0L, hashBlockSize);
-                            fail("Unexpected handle success on folder=" + srcFolder + ": " + result.getFirst());
-                        } catch(IOException e) {    // expected - not allowed to hash a folder
-                            assertTrue("Not an SftpException", e instanceof SftpException);
-                        }
-                    }
-                    
-                    validateHashResult(file, file.checkFileName(srcPath, algorithms, 0L, 0L, hashBlockSize), algorithms.get(0), expectedHash);
-                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Read)) {
-                        validateHashResult(hndl, hndl.checkFileHandle(fileHandle, algorithms, 0L, 0L, hashBlockSize), algorithms.get(0), expectedHash);
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-    
-    private void validateHashResult(NamedResource hasher, Pair<String,Collection<byte[]>> result, String expectedAlgorithm, byte[] expectedHash) {
-        String name = hasher.getName();
-        assertNotNull("No result for hash=" + name, result);
-        assertEquals("Mismatched hash algorithms for " + name, expectedAlgorithm, result.getFirst());
-        
-        if (GenericUtils.length(expectedHash) > 0) {
-            Collection<byte[]> values = result.getSecond();
-            assertEquals("Mismatched hash values count for " + name, 1, GenericUtils.size(values));
-            
-            byte[] actualHash = values.iterator().next();
-            if (!Arrays.equals(expectedHash, actualHash)) {
-                fail("Mismatched hashes for " + name
-                   + ": expected=" + BufferUtils.printHex(':', expectedHash)
-                   + ", actual=" + BufferUtils.printHex(':', expectedHash));
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java
deleted file mode 100644
index 08b8b5a..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java
+++ /dev/null
@@ -1,178 +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.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SftpException;
-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.digest.BuiltinDigests;
-import org.apache.sshd.common.digest.Digest;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.buffer.BufferUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-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.runner.RunWith;
-import org.junit.runners.MethodSorters;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
-public class AbstractMD5HashExtensionTest extends AbstractSftpClientTestSupport {
-    private static final List<Integer> DATA_SIZES =
-            Collections.unmodifiableList(
-                    Arrays.asList(
-                                Integer.valueOf(Byte.MAX_VALUE),
-                                Integer.valueOf(SftpConstants.MD5_QUICK_HASH_SIZE),
-                                Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
-                                Integer.valueOf(Byte.SIZE * IoUtils.DEFAULT_COPY_SIZE)
-                            ));
-
-    @Parameters(name = "dataSize={0}")
-    public static Collection<Object[]> parameters() {
-        return parameterize(DATA_SIZES);
-    }
-
-    private final int size;
-
-    public AbstractMD5HashExtensionTest(int size) throws IOException {
-        this.size = size;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        setupServer();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        tearDownServer();
-    }
-
-    @Test
-    public void testMD5HashExtension() throws Exception {
-        testMD5HashExtension(size);
-    }
-
-    private void testMD5HashExtension(int dataSize) throws Exception {
-        byte[] seed = (getClass().getName() + "#" + getCurrentTestName() + "-" + dataSize + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8);
-        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(dataSize + seed.length)) {
-            while (baos.size() < dataSize) {
-                baos.write(seed);
-            }
-
-            testMD5HashExtension(baos.toByteArray());
-        }
-    }
-
-    private void testMD5HashExtension(byte[] data) throws Exception {
-        Digest digest = BuiltinDigests.md5.create();
-        digest.init();
-        digest.update(data);
-
-        byte[] expectedHash = digest.digest();
-        byte[] quickHash = expectedHash;
-        if (data.length > SftpConstants.MD5_QUICK_HASH_SIZE) {
-            byte[] quickData = new byte[SftpConstants.MD5_QUICK_HASH_SIZE];
-            System.arraycopy(data, 0, quickData, 0, quickData.length);
-            digest = BuiltinDigests.md5.create();
-            digest.init();
-            digest.update(quickData);
-            quickHash = digest.digest();
-        }
-
-        Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
-        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve("data-" + data.length + ".txt");
-        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
-
-        Path parentPath = targetPath.getParent();
-        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
-        String srcFolder = Utils.resolveRelativeRemotePath(parentPath, srcFile.getParent());
-
-        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()) {
-                    MD5FileExtension file = assertExtensionCreated(sftp, MD5FileExtension.class);
-                    try {
-                        byte[] actual = file.getHash(srcFolder, 0L, 0L, quickHash);
-                        fail("Unexpected file success on folder=" + srcFolder + ": " + BufferUtils.printHex(':', actual));
-                    } catch(IOException e) {    // expected - not allowed to hash a folder
-                        assertTrue("Not an SftpException for file hash on " + srcFolder, e instanceof SftpException);
-                    }
-
-                    MD5HandleExtension hndl = assertExtensionCreated(sftp, MD5HandleExtension.class);
-                    try(CloseableHandle dirHandle = sftp.openDir(srcFolder)) {
-                        try {
-                            byte[] actual = hndl.getHash(dirHandle, 0L, 0L, quickHash);
-                            fail("Unexpected handle success on folder=" + srcFolder + ": " + BufferUtils.printHex(':', actual));
-                        } catch(IOException e) {    // expected - not allowed to hash a folder
-                            assertTrue("Not an SftpException for handle hash on " + srcFolder, e instanceof SftpException);
-                        }
-                    }
-
-                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Read)) {
-                        for (byte[] qh : new byte[][] { GenericUtils.EMPTY_BYTE_ARRAY, quickHash }) {
-                            for (boolean useFile : new boolean[] { true, false }) {
-                                byte[] actualHash = useFile ? file.getHash(srcPath, 0L, 0L, qh) : hndl.getHash(fileHandle, 0L, 0L, qh);
-                                String type = useFile ? file.getClass().getSimpleName() : hndl.getClass().getSimpleName();
-                                if (!Arrays.equals(expectedHash, actualHash)) {
-                                    fail("Mismatched hash for quick=" + BufferUtils.printHex(':', qh)
-                                       + " using " + type + " on " + srcFile
-                                       + ": expected=" + BufferUtils.printHex(':', expectedHash)
-                                       + ", actual=" + BufferUtils.printHex(':', actualHash));
-                                }
-                            }
-                        }
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyDataExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyDataExtensionImplTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyDataExtensionImplTest.java
deleted file mode 100644
index afea6df..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyDataExtensionImplTest.java
+++ /dev/null
@@ -1,200 +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.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-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.common.Factory;
-import org.apache.sshd.common.random.Random;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.util.io.IoUtils;
-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.runner.RunWith;
-import org.junit.runners.MethodSorters;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
-public class CopyDataExtensionImplTest extends AbstractSftpClientTestSupport {
-    private static final List<Object[]> PARAMETERS =
-            Collections.unmodifiableList(
-                    Arrays.<Object[]>asList(
-                                new Object[] {
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
-                                        Integer.valueOf(0),
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
-                                        Long.valueOf(0L)
-                                    },
-                                new Object[] {
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 2),
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 4),
-                                        Long.valueOf(0L)
-                                    },
-                                new Object[] {
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 2),
-                                        Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE / 4),
-                                        Long.valueOf(IoUtils.DEFAULT_COPY_SIZE / 2)
-                                    },
-                                new Object[] {
-                                        Integer.valueOf(Byte.MAX_VALUE),
-                                        Integer.valueOf(Byte.MAX_VALUE / 2),
-                                        Integer.valueOf(Byte.MAX_VALUE),    // attempt to read more than available
-                                        Long.valueOf(0L)
-                                    }
-                            ));
-
-    @Parameters(name = "size={0}, readOffset={1}, readLength={2}, writeOffset={3}")
-    public static Collection<Object[]> parameters() {
-        return PARAMETERS;
-    }
-
-    private int size, srcOffset, length;
-    private long dstOffset;
-
-    public CopyDataExtensionImplTest(int size, int srcOffset, int length, long dstOffset) throws IOException {
-        this.size = size;
-        this.srcOffset = srcOffset;
-        this.length = length;
-        this.dstOffset = dstOffset;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        setupServer();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        tearDownServer();
-    }
-
-    @Test
-    public void testCopyDataExtension() throws Exception {
-        testCopyDataExtension(size, srcOffset, length, dstOffset);
-    }
-
-    private void testCopyDataExtension(int dataSize, int readOffset, int readLength, long writeOffset) throws Exception {
-        byte[] seed = (getClass().getName() + "#" + getCurrentTestName()
-                     + "-" + dataSize
-                     + "-" + readOffset + "/" + readLength + "/" + writeOffset
-                     + System.getProperty("line.separator"))
-                .getBytes(StandardCharsets.UTF_8)
-                ;
-        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(dataSize + seed.length)) {
-            while (baos.size() < dataSize) {
-                baos.write(seed);
-            }
-            
-            testCopyDataExtension(baos.toByteArray(), readOffset, readLength, writeOffset);
-        }
-    }
-
-    private void testCopyDataExtension(byte[] data, int readOffset, int readLength, long writeOffset) throws Exception {
-        Path targetPath = detectTargetFolder().toPath();
-        Path parentPath = targetPath.getParent();
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
-        LinkOption[] options = IoUtils.getLinkOptions(false);
-        String baseName = readOffset + "-" + readLength + "-" + writeOffset;
-        Path srcFile = assertHierarchyTargetFolderExists(lclSftp, options).resolve(baseName + "-src.txt");
-        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
-        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
-
-        Path dstFile = srcFile.getParent().resolve(baseName + "-dst.txt");
-        if (Files.exists(dstFile, options)) {
-            Files.delete(dstFile);
-        }
-        String dstPath = Utils.resolveRelativeRemotePath(parentPath, dstFile);
-
-        try(SshClient client = SshClient.setUpDefaultClient()) {
-            client.start();
-
-            if (writeOffset > 0L) {
-                Factory<? extends Random> factory = client.getRandomFactory();
-                Random randomizer = factory.create();
-                long totalLength = writeOffset + readLength;
-                byte[] workBuf = new byte[(int) Math.min(totalLength, IoUtils.DEFAULT_COPY_SIZE)];
-                try(OutputStream output = Files.newOutputStream(dstFile, IoUtils.EMPTY_OPEN_OPTIONS)) {
-                    while(totalLength > 0L) {
-                        randomizer.fill(workBuf);
-                        output.write(workBuf);
-                        totalLength -= workBuf.length;
-                    }
-                }
-            }
-
-            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()) {
-                    CopyDataExtension ext = assertExtensionCreated(sftp, CopyDataExtension.class);
-                    try(CloseableHandle readHandle = sftp.open(srcPath, SftpClient.OpenMode.Read);
-                        CloseableHandle writeHandle = sftp.open(dstPath, SftpClient.OpenMode.Write, SftpClient.OpenMode.Create)) {
-                        ext.copyData(readHandle, readOffset, readLength, writeHandle, writeOffset);
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-        
-        int available = data.length, required = readOffset + readLength;
-        if (required > available) {
-            required = available; 
-        }
-        byte[] expected = new byte[required - readOffset];
-        System.arraycopy(data, readOffset, expected, 0, expected.length);
-        
-        byte[] actual = new byte[expected.length];
-        try(FileChannel channel = FileChannel.open(dstFile, IoUtils.EMPTY_OPEN_OPTIONS)) {
-            int readLen = channel.read(ByteBuffer.wrap(actual), writeOffset);
-            assertEquals("Mismatched read data size", expected.length, readLen);
-        }
-        assertArrayEquals("Mismatched copy data", expected, actual);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtensionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtensionTest.java
new file mode 100644
index 0000000..5662cf7
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtensionTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+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.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SftpException;
+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.CheckFileHandleExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.CheckFileNameExtension;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+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.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class AbstractCheckFileExtensionTest extends AbstractSftpClientTestSupport {
+    private static final Collection<Integer> DATA_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            Integer.valueOf(Byte.MAX_VALUE),
+                            Integer.valueOf(SftpConstants.MIN_CHKFILE_BLOCKSIZE),
+                            Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                            Integer.valueOf(Byte.SIZE *IoUtils.DEFAULT_COPY_SIZE)
+                    ));
+    private static final Collection<Integer> BLOCK_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            Integer.valueOf(0),
+                            Integer.valueOf(SftpConstants.MIN_CHKFILE_BLOCKSIZE),
+                            Integer.valueOf(1024),
+                            Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE)
+                        ));
+    @SuppressWarnings("synthetic-access")
+    private static final Collection<Object[]> PARAMETERS =
+            Collections.unmodifiableCollection(new LinkedList<Object[]>() {
+                private static final long serialVersionUID = 1L;    // we're not serializing it
+                
+                {
+                    for (NamedFactory<?> factory : BuiltinDigests.VALUES) {
+                        String algorithm = factory.getName();
+                        for (Number dataSize : DATA_SIZES) {
+                            for (Number blockSize : BLOCK_SIZES) {
+                                add(new Object[] { algorithm, dataSize, blockSize });
+                            }
+                        }
+                    }
+                }
+            });
+
+    @Parameters(name = "{0} - dataSize={1}, blockSize={2}")
+    public static Collection<Object[]> parameters() {
+        return PARAMETERS;
+    }
+
+    private final String algorithm;
+    private final int dataSize, blockSize;
+
+    public AbstractCheckFileExtensionTest(String algorithm, int dataSize, int blockSize) throws IOException {
+        this.algorithm = algorithm;
+        this.dataSize = dataSize;
+        this.blockSize = blockSize;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testCheckFileExtension() throws Exception {
+        testCheckFileExtension(algorithm, dataSize, blockSize);
+    }
+
+    private void testCheckFileExtension(String expectedAlgorithm, int inputDataSize, int hashBlockSize) throws Exception {
+        NamedFactory<? extends Digest> factory = BuiltinDigests.fromFactoryName(expectedAlgorithm);
+        Digest digest = null;
+        if (blockSize == 0) {
+            digest = factory.create();
+            digest.init();
+        }
+
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName()
+                     + "-" + expectedAlgorithm
+                     + "-" + inputDataSize + "/" + hashBlockSize
+                     + System.getProperty("line.separator"))
+                .getBytes(StandardCharsets.UTF_8)
+                ;
+
+        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(inputDataSize + seed.length)) {
+            while (baos.size() < inputDataSize) {
+                baos.write(seed);
+                
+                if (digest != null) {
+                    digest.update(seed);
+                }
+            }
+
+            testCheckFileExtension(factory, baos.toByteArray(), hashBlockSize, (digest == null) ? null : digest.digest());
+        }
+    }
+
+    private void testCheckFileExtension(NamedFactory<? extends Digest> factory, byte[] data, int hashBlockSize, byte[] expectedHash) throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve(factory.getName() + "-data-" + data.length + "-" + hashBlockSize + ".txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+
+        List<String> algorithms = new ArrayList<String>(BuiltinDigests.VALUES.size());
+        // put the selected algorithm 1st and then the rest
+        algorithms.add(factory.getName());
+        for (NamedFactory<? extends Digest> f : BuiltinDigests.VALUES) {
+            if (f == factory) {
+                continue;
+            }
+            
+            algorithms.add(f.getName());
+        }
+
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+        String srcFolder = Utils.resolveRelativeRemotePath(parentPath, srcFile.getParent());
+
+        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()) {
+                    CheckFileNameExtension file = assertExtensionCreated(sftp, CheckFileNameExtension.class);
+                    try {
+                        Pair<String,?> result = file.checkFileName(srcFolder, algorithms, 0L, 0L, hashBlockSize);
+                        fail("Unexpected success to hash folder=" + srcFolder + ": " + result.getFirst());
+                    } catch(IOException e) {    // expected - not allowed to hash a folder
+                        assertTrue("Not an SftpException", e instanceof SftpException);
+                    }
+
+                    CheckFileHandleExtension hndl = assertExtensionCreated(sftp, CheckFileHandleExtension.class);
+                    try(CloseableHandle dirHandle = sftp.openDir(srcFolder)) {
+                        try {
+                            Pair<String,?> result = hndl.checkFileHandle(dirHandle, algorithms, 0L, 0L, hashBlockSize);
+                            fail("Unexpected handle success on folder=" + srcFolder + ": " + result.getFirst());
+                        } catch(IOException e) {    // expected - not allowed to hash a folder
+                            assertTrue("Not an SftpException", e instanceof SftpException);
+                        }
+                    }
+                    
+                    validateHashResult(file, file.checkFileName(srcPath, algorithms, 0L, 0L, hashBlockSize), algorithms.get(0), expectedHash);
+                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Read)) {
+                        validateHashResult(hndl, hndl.checkFileHandle(fileHandle, algorithms, 0L, 0L, hashBlockSize), algorithms.get(0), expectedHash);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+    
+    private void validateHashResult(NamedResource hasher, Pair<String,Collection<byte[]>> result, String expectedAlgorithm, byte[] expectedHash) {
+        String name = hasher.getName();
+        assertNotNull("No result for hash=" + name, result);
+        assertEquals("Mismatched hash algorithms for " + name, expectedAlgorithm, result.getFirst());
+        
+        if (GenericUtils.length(expectedHash) > 0) {
+            Collection<byte[]> values = result.getSecond();
+            assertEquals("Mismatched hash values count for " + name, 1, GenericUtils.size(values));
+            
+            byte[] actualHash = values.iterator().next();
+            if (!Arrays.equals(expectedHash, actualHash)) {
+                fail("Mismatched hashes for " + name
+                   + ": expected=" + BufferUtils.printHex(':', expectedHash)
+                   + ", actual=" + BufferUtils.printHex(':', expectedHash));
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ea46a25d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtensionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtensionTest.java
new file mode 100644
index 0000000..87f1510
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtensionTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SftpException;
+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.MD5FileExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.MD5HandleExtension;
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+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.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class AbstractMD5HashExtensionTest extends AbstractSftpClientTestSupport {
+    private static final List<Integer> DATA_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                                Integer.valueOf(Byte.MAX_VALUE),
+                                Integer.valueOf(SftpConstants.MD5_QUICK_HASH_SIZE),
+                                Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                                Integer.valueOf(Byte.SIZE * IoUtils.DEFAULT_COPY_SIZE)
+                            ));
+
+    @Parameters(name = "dataSize={0}")
+    public static Collection<Object[]> parameters() {
+        return parameterize(DATA_SIZES);
+    }
+
+    private final int size;
+
+    public AbstractMD5HashExtensionTest(int size) throws IOException {
+        this.size = size;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testMD5HashExtension() throws Exception {
+        testMD5HashExtension(size);
+    }
+
+    private void testMD5HashExtension(int dataSize) throws Exception {
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName() + "-" + dataSize + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8);
+        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(dataSize + seed.length)) {
+            while (baos.size() < dataSize) {
+                baos.write(seed);
+            }
+
+            testMD5HashExtension(baos.toByteArray());
+        }
+    }
+
+    private void testMD5HashExtension(byte[] data) throws Exception {
+        Digest digest = BuiltinDigests.md5.create();
+        digest.init();
+        digest.update(data);
+
+        byte[] expectedHash = digest.digest();
+        byte[] quickHash = expectedHash;
+        if (data.length > SftpConstants.MD5_QUICK_HASH_SIZE) {
+            byte[] quickData = new byte[SftpConstants.MD5_QUICK_HASH_SIZE];
+            System.arraycopy(data, 0, quickData, 0, quickData.length);
+            digest = BuiltinDigests.md5.create();
+            digest.init();
+            digest.update(quickData);
+            quickHash = digest.digest();
+        }
+
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path srcFile = assertHierarchyTargetFolderExists(lclSftp).resolve("data-" + data.length + ".txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+        String srcFolder = Utils.resolveRelativeRemotePath(parentPath, srcFile.getParent());
+
+        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()) {
+                    MD5FileExtension file = assertExtensionCreated(sftp, MD5FileExtension.class);
+                    try {
+                        byte[] actual = file.getHash(srcFolder, 0L, 0L, quickHash);
+                        fail("Unexpected file success on folder=" + srcFolder + ": " + BufferUtils.printHex(':', actual));
+                    } catch(IOException e) {    // expected - not allowed to hash a folder
+                        assertTrue("Not an SftpException for file hash on " + srcFolder, e instanceof SftpException);
+                    }
+
+                    MD5HandleExtension hndl = assertExtensionCreated(sftp, MD5HandleExtension.class);
+                    try(CloseableHandle dirHandle = sftp.openDir(srcFolder)) {
+                        try {
+                            byte[] actual = hndl.getHash(dirHandle, 0L, 0L, quickHash);
+                            fail("Unexpected handle success on folder=" + srcFolder + ": " + BufferUtils.printHex(':', actual));
+                        } catch(IOException e) {    // expected - not allowed to hash a folder
+                            assertTrue("Not an SftpException for handle hash on " + srcFolder, e instanceof SftpException);
+                        }
+                    }
+
+                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Read)) {
+                        for (byte[] qh : new byte[][] { GenericUtils.EMPTY_BYTE_ARRAY, quickHash }) {
+                            for (boolean useFile : new boolean[] { true, false }) {
+                                byte[] actualHash = useFile ? file.getHash(srcPath, 0L, 0L, qh) : hndl.getHash(fileHandle, 0L, 0L, qh);
+                                String type = useFile ? file.getClass().getSimpleName() : hndl.getClass().getSimpleName();
+                                if (!Arrays.equals(expectedHash, actualHash)) {
+                                    fail("Mismatched hash for quick=" + BufferUtils.printHex(':', qh)
+                                       + " using " + type + " on " + srcFile
+                                       + ": expected=" + BufferUtils.printHex(':', expectedHash)
+                                       + ", actual=" + BufferUtils.printHex(':', actualHash));
+                                }
+                            }
+                        }
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}