You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2018/04/16 11:47:50 UTC

[03/30] mina-sshd git commit: [SSHD-815] Extract SFTP in its own module

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
new file mode 100644
index 0000000..e29b732
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
@@ -0,0 +1,510 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryFlag;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.OpenMode;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpHelper;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.subsystem.sftp.AbstractSftpEventListenerAdapter;
+import org.apache.sshd.server.subsystem.sftp.DefaultGroupPrincipal;
+import org.apache.sshd.server.subsystem.sftp.SftpEventListener;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystem;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemEnvironment;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.Utils;
+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;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @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
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+public class SftpVersionsTest extends AbstractSftpClientTestSupport {
+    private static final List<Integer> VERSIONS =
+        Collections.unmodifiableList(
+            IntStream.rangeClosed(SftpSubsystemEnvironment.LOWER_SFTP_IMPL, SftpSubsystemEnvironment.HIGHER_SFTP_IMPL)
+                .boxed()
+                .collect(Collectors.toList()));
+
+    private final int testVersion;
+
+    public SftpVersionsTest(int version) throws IOException {
+        testVersion = version;
+    }
+
+    @Parameters(name = "version={0}")
+    public static Collection<Object[]> parameters() {
+        return parameterize(VERSIONS);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    public final int getTestedVersion() {
+        return testVersion;
+    }
+
+    @Test   // See SSHD-749
+    public void testSftpOpenFlags() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path lclParent = assertHierarchyTargetFolderExists(lclSftp);
+        Path lclFile = lclParent.resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt");
+        Files.deleteIfExists(lclFile);
+
+        Path parentPath = targetPath.getParent();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclFile);
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                try (OutputStream out = sftp.write(remotePath, OpenMode.Create, OpenMode.Write)) {
+                    out.write(getCurrentTestName().getBytes(StandardCharsets.UTF_8));
+                }
+                assertTrue("File should exist on disk: " + lclFile, Files.exists(lclFile));
+                sftp.remove(remotePath);
+            }
+        }
+    }
+
+    @Test
+    public void testSftpVersionSelector() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                assertEquals("Mismatched negotiated version", getTestedVersion(), sftp.getVersion());
+            }
+        }
+    }
+
+    @Test   // see SSHD-572
+    public void testSftpFileTimesUpdate() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt");
+        Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8));
+        Path parentPath = targetPath.getParent();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclFile);
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                Attributes attrs = sftp.lstat(remotePath);
+                long expectedSeconds = TimeUnit.SECONDS.convert(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1L), TimeUnit.MILLISECONDS);
+                attrs.getFlags().clear();
+                attrs.modifyTime(expectedSeconds);
+                sftp.setStat(remotePath, attrs);
+
+                attrs = sftp.lstat(remotePath);
+                long actualSeconds = attrs.getModifyTime().to(TimeUnit.SECONDS);
+                // The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access
+                if (expectedSeconds != actualSeconds) {
+                    System.err.append("Mismatched last modified time for ").append(lclFile.toString())
+                              .append(" - expected=").append(String.valueOf(expectedSeconds))
+                              .append('[').append(new Date(TimeUnit.SECONDS.toMillis(expectedSeconds)).toString()).append(']')
+                              .append(", actual=").append(String.valueOf(actualSeconds))
+                              .append('[').append(new Date(TimeUnit.SECONDS.toMillis(actualSeconds)).toString()).append(']')
+                              .println();
+                }
+            }
+        }
+    }
+
+    @Test   // see SSHD-573
+    public void testSftpFileTypeAndPermissionsUpdate() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Path subFolder = Files.createDirectories(lclSftp.resolve("sub-folder"));
+        String subFolderName = subFolder.getFileName().toString();
+        Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt");
+        String lclFileName = lclFile.getFileName().toString();
+        Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8));
+
+        Path parentPath = targetPath.getParent();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp);
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                for (DirEntry entry : sftp.readDir(remotePath)) {
+                    String fileName = entry.getFilename();
+                    if (".".equals(fileName) || "..".equals(fileName)) {
+                        continue;
+                    }
+
+                    Attributes attrs = validateSftpFileTypeAndPermissions(fileName, getTestedVersion(), entry.getAttributes());
+                    if (subFolderName.equals(fileName)) {
+                        assertEquals("Mismatched sub-folder type", SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY, attrs.getType());
+                        assertTrue("Sub-folder not marked as directory", attrs.isDirectory());
+                    } else if (lclFileName.equals(fileName)) {
+                        assertEquals("Mismatched sub-file type", SftpConstants.SSH_FILEXFER_TYPE_REGULAR, attrs.getType());
+                        assertTrue("Sub-folder not marked as directory", attrs.isRegularFile());
+                    }
+                }
+            }
+        }
+    }
+
+    @Test   // see SSHD-574
+    public void testSftpACLEncodeDecode() throws Exception {
+        AclEntryType[] types = AclEntryType.values();
+        final List<AclEntry> aclExpected = new ArrayList<>(types.length);
+        for (AclEntryType t : types) {
+            aclExpected.add(AclEntry.newBuilder()
+                                .setType(t)
+                                .setFlags(EnumSet.allOf(AclEntryFlag.class))
+                                .setPermissions(EnumSet.allOf(AclEntryPermission.class))
+                                .setPrincipal(new DefaultGroupPrincipal(getCurrentTestName() + "@" + getClass().getPackage().getName()))
+                                .build());
+        }
+
+        final AtomicInteger numInvocations = new AtomicInteger(0);
+        SftpSubsystemFactory factory = new SftpSubsystemFactory() {
+            @Override
+            public Command create() {
+                SftpSubsystem subsystem = new SftpSubsystem(getExecutorService(), isShutdownOnExit(),
+                        getUnsupportedAttributePolicy(), getFileSystemAccessor(), getErrorStatusDataHandler()) {
+                    @Override
+                    protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options) throws IOException {
+                        NavigableMap<String, Object> attrs = super.resolveFileAttributes(file, flags, options);
+                        if (GenericUtils.isEmpty(attrs)) {
+                            attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+                        }
+
+                        @SuppressWarnings("unchecked")
+                        List<AclEntry> aclActual = (List<AclEntry>) attrs.put("acl", aclExpected);
+                        if (aclActual != null) {
+                            log.info("resolveFileAttributes(" + file + ") replaced ACL: " + aclActual);
+                        }
+                        return attrs;
+                    }
+
+                    @Override
+                    protected void setFileAccessControl(Path file, List<AclEntry> aclActual, LinkOption... options) throws IOException {
+                        if (aclActual != null) {
+                            assertListEquals("Mismatched ACL set for file=" + file, aclExpected, aclActual);
+                            numInvocations.incrementAndGet();
+                        }
+                    }
+                };
+                Collection<? extends SftpEventListener> listeners = getRegisteredListeners();
+                if (GenericUtils.size(listeners) > 0) {
+                    for (SftpEventListener l : listeners) {
+                        subsystem.addSftpEventListener(l);
+                    }
+                }
+
+                return subsystem;
+            }
+        };
+
+        factory.addSftpEventListener(new AbstractSftpEventListenerAdapter() {
+            @Override
+            public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) {
+                @SuppressWarnings("unchecked")
+                List<AclEntry> aclActual = GenericUtils.isEmpty(attrs) ? null : (List<AclEntry>) attrs.get("acl");
+                if (getTestedVersion() > SftpConstants.SFTP_V3) {
+                    assertListEquals("Mismatched modifying ACL for file=" + path, aclExpected, aclActual);
+                } else {
+                    assertNull("Unexpected modifying ACL for file=" + path, aclActual);
+                }
+            }
+
+            @Override
+            public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) {
+                @SuppressWarnings("unchecked")
+                List<AclEntry> aclActual  = GenericUtils.isEmpty(attrs) ? null : (List<AclEntry>) attrs.get("acl");
+                if (getTestedVersion() > SftpConstants.SFTP_V3) {
+                    assertListEquals("Mismatched modified ACL for file=" + path, aclExpected, aclActual);
+                } else {
+                    assertNull("Unexpected modified ACL for file=" + path, aclActual);
+                }
+            }
+        });
+
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Files.createDirectories(lclSftp.resolve("sub-folder"));
+        Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt");
+        Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8));
+
+        Path parentPath = targetPath.getParent();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp);
+        int numInvoked = 0;
+
+        List<NamedFactory<Command>> factories = sshd.getSubsystemFactories();
+        sshd.setSubsystemFactories(Collections.singletonList(factory));
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                for (DirEntry entry : sftp.readDir(remotePath)) {
+                    String fileName = entry.getFilename();
+                    if (".".equals(fileName) || "..".equals(fileName)) {
+                        continue;
+                    }
+
+                    Attributes attrs = validateSftpFileTypeAndPermissions(fileName, getTestedVersion(), entry.getAttributes());
+                    List<AclEntry> aclActual = attrs.getAcl();
+                    if (getTestedVersion() == SftpConstants.SFTP_V3) {
+                        assertNull("Unexpected ACL for entry=" + fileName, aclActual);
+                    } else {
+                        assertListEquals("Mismatched ACL for entry=" + fileName, aclExpected, aclActual);
+                    }
+
+                    attrs.getFlags().clear();
+                    attrs.setAcl(aclExpected);
+                    sftp.setStat(remotePath + "/" + fileName, attrs);
+                    if (getTestedVersion() > SftpConstants.SFTP_V3) {
+                        numInvoked++;
+                    }
+                }
+            }
+        } finally {
+            sshd.setSubsystemFactories(factories);
+        }
+
+        assertEquals("Mismatched invocations count", numInvoked, numInvocations.get());
+    }
+
+    @Test   // see SSHD-575
+    public void testSftpExtensionsEncodeDecode() throws Exception {
+        final Class<?> anchor = getClass();
+        final Map<String, String> expExtensions = GenericUtils.<String, String>mapBuilder()
+                .put("class", anchor.getSimpleName())
+                .put("package", anchor.getPackage().getName())
+                .put("method", getCurrentTestName())
+                .build();
+
+        final AtomicInteger numInvocations = new AtomicInteger(0);
+        SftpSubsystemFactory factory = new SftpSubsystemFactory() {
+            @Override
+            public Command create() {
+                SftpSubsystem subsystem = new SftpSubsystem(getExecutorService(), isShutdownOnExit(),
+                        getUnsupportedAttributePolicy(), getFileSystemAccessor(), getErrorStatusDataHandler()) {
+                    @Override
+                    protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options) throws IOException {
+                        NavigableMap<String, Object> attrs = super.resolveFileAttributes(file, flags, options);
+                        if (GenericUtils.isEmpty(attrs)) {
+                            attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+                        }
+
+                        @SuppressWarnings("unchecked")
+                        Map<String, String> actExtensions = (Map<String, String>) attrs.put("extended", expExtensions);
+                        if (actExtensions != null) {
+                            log.info("resolveFileAttributes(" + file + ") replaced extensions: " + actExtensions);
+                        }
+                        return attrs;
+                    }
+
+                    @Override
+                    protected void setFileExtensions(Path file, Map<String, byte[]> extensions, LinkOption... options) throws IOException {
+                        assertExtensionsMapEquals("setFileExtensions(" + file + ")", expExtensions, extensions);
+                        numInvocations.incrementAndGet();
+
+                        int currentVersion = getTestedVersion();
+                        try {
+                            super.setFileExtensions(file, extensions, options);
+                            assertFalse("Expected exception not generated for version=" + currentVersion, currentVersion >= SftpConstants.SFTP_V6);
+                        } catch (UnsupportedOperationException e) {
+                            assertTrue("Unexpected exception for version=" + currentVersion, currentVersion >= SftpConstants.SFTP_V6);
+                        }
+                    }
+                };
+                Collection<? extends SftpEventListener> listeners = getRegisteredListeners();
+                if (GenericUtils.size(listeners) > 0) {
+                    for (SftpEventListener l : listeners) {
+                        subsystem.addSftpEventListener(l);
+                    }
+                }
+
+                return subsystem;
+            }
+        };
+
+        factory.addSftpEventListener(new AbstractSftpEventListenerAdapter() {
+            @Override
+            public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) {
+                @SuppressWarnings("unchecked")
+                Map<String, byte[]> actExtensions = GenericUtils.isEmpty(attrs) ? null : (Map<String, byte[]>) attrs.get("extended");
+                assertExtensionsMapEquals("modifying(" + path + ")", expExtensions, actExtensions);
+            }
+
+            @Override
+            public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) {
+                @SuppressWarnings("unchecked")
+                Map<String, byte[]> actExtensions = GenericUtils.isEmpty(attrs) ? null : (Map<String, byte[]>) attrs.get("extended");
+                assertExtensionsMapEquals("modified(" + path + ")", expExtensions, actExtensions);
+            }
+        });
+
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        Files.createDirectories(lclSftp.resolve("sub-folder"));
+        Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt");
+        Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8));
+
+        Path parentPath = targetPath.getParent();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp);
+        int numInvoked = 0;
+
+        List<NamedFactory<Command>> factories = sshd.getSubsystemFactories();
+        sshd.setSubsystemFactories(Collections.singletonList(factory));
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                for (DirEntry entry : sftp.readDir(remotePath)) {
+                    String fileName = entry.getFilename();
+                    if (".".equals(fileName) || "..".equals(fileName)) {
+                        continue;
+                    }
+
+                    Attributes attrs = validateSftpFileTypeAndPermissions(fileName, getTestedVersion(), entry.getAttributes());
+                    Map<String, byte[]> actExtensions = attrs.getExtensions();
+                    assertExtensionsMapEquals("dirEntry=" + fileName, expExtensions, actExtensions);
+                    attrs.getFlags().clear();
+                    attrs.setStringExtensions(expExtensions);
+                    sftp.setStat(remotePath + "/" + fileName, attrs);
+                    numInvoked++;
+                }
+            }
+        } finally {
+            sshd.setSubsystemFactories(factories);
+        }
+
+        assertEquals("Mismatched invocations count", numInvoked, numInvocations.get());
+    }
+
+    @Test   // see SSHD-623
+    public void testEndOfListIndicator() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session, getTestedVersion())) {
+                AtomicReference<Boolean> eolIndicator = new AtomicReference<>();
+                int version = sftp.getVersion();
+                Path targetPath = detectTargetFolder();
+                Path parentPath = targetPath.getParent();
+                String remotePath = Utils.resolveRelativeRemotePath(parentPath, targetPath);
+
+                try (CloseableHandle handle = sftp.openDir(remotePath)) {
+                    List<DirEntry> entries = sftp.readDir(handle, eolIndicator);
+                    for (int index = 1; entries != null; entries = sftp.readDir(handle, eolIndicator), index++) {
+                        Boolean value = eolIndicator.get();
+                        if (version < SftpConstants.SFTP_V6) {
+                            assertNull("Unexpected indicator value at iteration #" + index, value);
+                        } else {
+                            assertNotNull("No indicator returned at iteration #" + index, value);
+                            if (value) {
+                                break;
+                            }
+                        }
+                        eolIndicator.set(null);    // make sure starting fresh
+                    }
+
+                    Boolean value = eolIndicator.get();
+                    if (version < SftpConstants.SFTP_V6) {
+                        assertNull("Unexpected end-of-list indication received at end of entries", value);
+                        assertNull("Unexpected no last entries indication", entries);
+                    } else {
+                        assertNotNull("No end-of-list indication received at end of entries", value);
+                        assertNotNull("No last received entries", entries);
+                        assertTrue("Bad end-of-list value", value);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + getTestedVersion() + "]";
+    }
+
+    public static void assertExtensionsMapEquals(String message, Map<String, String> expected, Map<String, byte[]> actual) {
+        assertMapEquals(message, expected, SftpHelper.toStringExtensions(actual));
+    }
+
+    private static Attributes validateSftpFileTypeAndPermissions(String fileName, int version, Attributes attrs) {
+        int actualPerms = attrs.getPermissions();
+        if (version == SftpConstants.SFTP_V3) {
+            int expected = SftpHelper.permissionsToFileType(actualPerms);
+            assertEquals(fileName + ": Mismatched file type", expected, attrs.getType());
+        } else {
+            int expected = SftpHelper.fileTypeToPermission(attrs.getType());
+            assertTrue(fileName + ": Missing permision=0x" + Integer.toHexString(expected) + " in 0x" + Integer.toHexString(actualPerms),
+                       (actualPerms & expected) == expected);
+        }
+
+        return attrs;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java
new file mode 100644
index 0000000..e05105d
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class BuiltinSftpClientExtensionsTest extends BaseTestSupport {
+    public BuiltinSftpClientExtensionsTest() {
+        super();
+    }
+
+    @Test
+    public void testFromName() {
+        for (String name : new String[]{null, "", getCurrentTestName()}) {
+            assertNull("Unexpected result for name='" + name + "'", BuiltinSftpClientExtensions.fromName(name));
+        }
+
+        for (BuiltinSftpClientExtensions expected : BuiltinSftpClientExtensions.VALUES) {
+            String name = expected.getName();
+            for (int index = 0; index < name.length(); index++) {
+                BuiltinSftpClientExtensions actual = BuiltinSftpClientExtensions.fromName(name);
+                assertSame(name, expected, actual);
+                name = shuffleCase(name);
+            }
+        }
+    }
+
+    @Test
+    public void testFromType() {
+        for (Class<?> clazz : new Class<?>[]{null, getClass(), SftpClientExtension.class}) {
+            assertNull("Unexpected value for class=" + clazz, BuiltinSftpClientExtensions.fromType(clazz));
+        }
+
+        for (BuiltinSftpClientExtensions expected : BuiltinSftpClientExtensions.VALUES) {
+            Class<?> type = expected.getType();
+            BuiltinSftpClientExtensions actual = BuiltinSftpClientExtensions.fromType(type);
+            assertSame(type.getSimpleName(), expected, actual);
+        }
+    }
+
+    @Test
+    public void testFromInstance() {
+        for (Object instance : new Object[]{null, this}) {
+            assertNull("Unexpected value for " + instance, BuiltinSftpClientExtensions.fromInstance(instance));
+        }
+
+        SftpClient mockClient = Mockito.mock(SftpClient.class);
+        RawSftpClient mockRaw = Mockito.mock(RawSftpClient.class);
+
+        for (BuiltinSftpClientExtensions expected : BuiltinSftpClientExtensions.VALUES) {
+            SftpClientExtension e = expected.create(mockClient, mockRaw);
+            BuiltinSftpClientExtensions actual = BuiltinSftpClientExtensions.fromInstance(e);
+            assertSame(expected.getName(), expected, actual);
+            assertEquals("Mismatched extension name", expected.getName(), actual.getName());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractCheckFileExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractCheckFileExtensionTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractCheckFileExtensionTest.java
new file mode 100644
index 0000000..e3537ea
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractCheckFileExtensionTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.helpers;
+
+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.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+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.digest.DigestFactory;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.Utils;
+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;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @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
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+public class AbstractCheckFileExtensionTest extends AbstractSftpClientTestSupport {
+    private static final Collection<Integer> DATA_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            (int) Byte.MAX_VALUE,
+                            SftpConstants.MIN_CHKFILE_BLOCKSIZE,
+                            IoUtils.DEFAULT_COPY_SIZE,
+                            Byte.SIZE * IoUtils.DEFAULT_COPY_SIZE
+                    ));
+    private static final Collection<Integer> BLOCK_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            0,
+                            SftpConstants.MIN_CHKFILE_BLOCKSIZE,
+                            1024,
+                            IoUtils.DEFAULT_COPY_SIZE
+                    ));
+    private static final Collection<Object[]> PARAMETERS;
+
+    static {
+        Collection<Object[]> list = new ArrayList<>();
+        for (DigestFactory factory : BuiltinDigests.VALUES) {
+            if (!factory.isSupported()) {
+                System.out.println("Skip unsupported digest=" + factory.getAlgorithm());
+                continue;
+            }
+
+            String algorithm = factory.getName();
+            for (Number dataSize : DATA_SIZES) {
+                for (Number blockSize : BLOCK_SIZES) {
+                    list.add(new Object[]{algorithm, dataSize, blockSize});
+                }
+            }
+        }
+        PARAMETERS = list;
+    }
+
+
+    private final String algorithm;
+    private final int dataSize;
+    private final int blockSize;
+
+    public AbstractCheckFileExtensionTest(String algorithm, int dataSize, int blockSize) throws IOException {
+        this.algorithm = algorithm;
+        this.dataSize = dataSize;
+        this.blockSize = blockSize;
+    }
+
+    @Parameters(name = "{0} - dataSize={1}, blockSize={2}")
+    public static Collection<Object[]> parameters() {
+        return PARAMETERS;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @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
+                + IoUtils.EOL)
+                .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());
+        }
+    }
+
+    @SuppressWarnings("checkstyle:nestedtrydepth")
+    private void testCheckFileExtension(NamedFactory<? extends Digest> factory, byte[] data, int hashBlockSize, byte[] expectedHash) throws Exception {
+        Path targetPath = detectTargetFolder();
+        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<>(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 (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session)) {
+                CheckFileNameExtension file = assertExtensionCreated(sftp, CheckFileNameExtension.class);
+                try {
+                    Map.Entry<String, ?> result = file.checkFileName(srcFolder, algorithms, 0L, 0L, hashBlockSize);
+                    fail("Unexpected success to hash folder=" + srcFolder + ": " + result.getKey());
+                } 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 {
+                        Map.Entry<String, ?> result = hndl.checkFileHandle(dirHandle, algorithms, 0L, 0L, hashBlockSize);
+                        fail("Unexpected handle success on folder=" + srcFolder + ": " + result.getKey());
+                    } 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);
+                }
+            }
+        }
+    }
+
+    private void validateHashResult(NamedResource hasher, Map.Entry<String, ? extends 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.getKey());
+
+        if (NumberUtils.length(expectedHash) > 0) {
+            Collection<byte[]> values = result.getValue();
+            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.toHex(':', expectedHash)
+                    + ", actual=" + BufferUtils.toHex(':', expectedHash));
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractMD5HashExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractMD5HashExtensionTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractMD5HashExtensionTest.java
new file mode 100644
index 0000000..ea2783a
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/AbstractMD5HashExtensionTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.helpers;
+
+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.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.subsystem.sftp.SftpException;
+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.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.Utils;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+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;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @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
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+public class AbstractMD5HashExtensionTest extends AbstractSftpClientTestSupport {
+    private static final List<Integer> DATA_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            (int) Byte.MAX_VALUE,
+                            SftpConstants.MD5_QUICK_HASH_SIZE,
+                            IoUtils.DEFAULT_COPY_SIZE,
+                            Byte.SIZE * IoUtils.DEFAULT_COPY_SIZE
+                    ));
+
+    private final int size;
+
+    public AbstractMD5HashExtensionTest(int size) throws IOException {
+        this.size = size;
+    }
+
+    @Parameters(name = "dataSize={0}")
+    public static Collection<Object[]> parameters() {
+        return parameterize(DATA_SIZES);
+    }
+
+    @BeforeClass
+    public static void checkMD5Supported() {
+        Assume.assumeTrue("MD5 not supported", BuiltinDigests.md5.isSupported());
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @Test
+    public void testMD5HashExtension() throws Exception {
+        testMD5HashExtension(size);
+    }
+
+    private void testMD5HashExtension(int dataSize) throws Exception {
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName() + "-" + dataSize + IoUtils.EOL).getBytes(StandardCharsets.UTF_8);
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream(dataSize + seed.length)) {
+            while (baos.size() < dataSize) {
+                baos.write(seed);
+            }
+
+            testMD5HashExtension(baos.toByteArray());
+        }
+    }
+
+    @SuppressWarnings("checkstyle:nestedtrydepth")
+    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();
+        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 (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session)) {
+                MD5FileExtension file = assertExtensionCreated(sftp, MD5FileExtension.class);
+                try {
+                    byte[] actual = file.getHash(srcFolder, 0L, 0L, quickHash);
+                    fail("Unexpected file success on folder=" + srcFolder + ": " + BufferUtils.toHex(':', 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.toHex(':', 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.toHex(':', qh)
+                                        + " using " + type + " on " + srcFile
+                                        + ": expected=" + BufferUtils.toHex(':', expectedHash)
+                                        + ", actual=" + BufferUtils.toHex(':', actualHash));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyDataExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyDataExtensionImplTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyDataExtensionImplTest.java
new file mode 100644
index 0000000..01d3334
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyDataExtensionImplTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.helpers;
+
+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.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.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.Utils;
+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;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @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
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+public class CopyDataExtensionImplTest extends AbstractSftpClientTestSupport {
+    private static final List<Object[]> PARAMETERS =
+            Collections.unmodifiableList(
+                    Arrays.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)
+                            }
+                    ));
+
+    private int size;
+    private int srcOffset;
+    private int  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;
+    }
+
+    @Parameters(name = "size={0}, readOffset={1}, readLength={2}, writeOffset={3}")
+    public static Collection<Object[]> parameters() {
+        return PARAMETERS;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @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
+                + IoUtils.EOL)
+                .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();
+        Path parentPath = targetPath.getParent();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
+        LinkOption[] options = IoUtils.getLinkOptions(true);
+        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);
+        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(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session)) {
+                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);
+                }
+            }
+        }
+
+        int available = data.length;
+        int 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/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyFileExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyFileExtensionImplTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyFileExtensionImplTest.java
new file mode 100644
index 0000000..b21da13
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/CopyFileExtensionImplTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.helpers;
+
+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.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.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.util.test.Utils;
+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();
+    }
+
+    @Test
+    public void testCopyFileExtension() throws Exception {
+        Path targetPath = detectTargetFolder();
+        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(true);
+        assertFalse("Destination file unexpectedly exists", Files.exists(dstFile, options));
+
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session)) {
+                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);
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/SpaceAvailableExtensionImplTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/SpaceAvailableExtensionImplTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/SpaceAvailableExtensionImplTest.java
new file mode 100644
index 0000000..0c33113
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/helpers/SpaceAvailableExtensionImplTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.helpers;
+
+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.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+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.test.Utils;
+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();
+    }
+
+    @Test
+    public void testFileStoreReport() throws Exception {
+        Path targetPath = detectTargetFolder();
+        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);
+
+        List<NamedFactory<Command>> factories = sshd.getSubsystemFactories();
+        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory() {
+            @Override
+            public Command create() {
+                return new SftpSubsystem(getExecutorService(), isShutdownOnExit(),
+                        getUnsupportedAttributePolicy(), getFileSystemAccessor(), getErrorStatusDataHandler()) {
+                    @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 (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session)) {
+                SpaceAvailableExtension ext = assertExtensionCreated(sftp, SpaceAvailableExtension.class);
+                SpaceAvailableExtensionInfo actual = ext.available(queryPath);
+                assertEquals("Mismatched information", expected, actual);
+            }
+        } finally {
+            sshd.setSubsystemFactories(factories);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/helpers/OpenSSHExtensionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/helpers/OpenSSHExtensionsTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/helpers/OpenSSHExtensionsTest.java
new file mode 100644
index 0000000..ac8ed34
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/openssh/helpers/OpenSSHExtensionsTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.helpers;
+
+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.Collections;
+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.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.session.ServerSession;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystem;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.Utils;
+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();
+    }
+
+    @Test
+    public void testFsync() throws IOException {
+        Path targetPath = detectTargetFolder();
+        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 (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            try (SftpClient sftp = createSftpClient(session)) {
+                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);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testStat() throws Exception {
+        Path targetPath = detectTargetFolder();
+        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<>(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(Collections.singletonList(new SftpSubsystemFactory() {
+            @Override
+            public Command create() {
+                return new SftpSubsystem(getExecutorService(), isShutdownOnExit(),
+                        getUnsupportedAttributePolicy(), getFileSystemAccessor(), getErrorStatusDataHandler()) {
+                    @Override
+                    protected List<OpenSSHExtension> resolveOpenSSHExtensions(ServerSession session) {
+                        List<OpenSSHExtension> original = super.resolveOpenSSHExtensions(session);
+                        int numOriginal = GenericUtils.size(original);
+                        List<OpenSSHExtension> result = new ArrayList<>(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) SftpConstants.SSH_FXP_EXTENDED_REPLY);
+                            buffer.putInt(id);
+                            OpenSSHStatExtensionInfo.encode(buffer, expected);
+                            send(buffer);
+                        } else {
+                            super.executeExtendedCommand(buffer, id, extension);
+                        }
+                    }
+                };
+            }
+        }));
+
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try (SftpClient sftp = createSftpClient(session)) {
+                    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);
+                        actual = handleStat.stat(handle);
+                        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);
+            Object actValue = f.get(actual);
+            assertEquals(extension + "[" + name + "]", expValue, actValue);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpConstantsTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpConstantsTest.java b/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpConstantsTest.java
new file mode 100644
index 0000000..d059d36
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpConstantsTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class SftpConstantsTest extends BaseTestSupport {
+    public SftpConstantsTest() {
+        super();
+    }
+
+    @Test
+    public void testRenameModesNotMarkedAsOpcodes() {
+        for (int cmd : new int[]{
+            SftpConstants.SSH_FXP_RENAME_OVERWRITE,
+            SftpConstants.SSH_FXP_RENAME_ATOMIC,
+            SftpConstants.SSH_FXP_RENAME_NATIVE
+        }) {
+            String name = SftpConstants.getCommandMessageName(cmd);
+            assertFalse("Mismatched name for " + cmd + ": " + name, name.startsWith("SSH_FXP_RENAME_"));
+        }
+    }
+
+    @Test
+    public void testRealPathModesNotMarkedAsOpcodes() {
+        for (int cmd = SftpConstants.SSH_FXP_REALPATH_NO_CHECK; cmd <= SftpConstants.SSH_FXP_REALPATH_STAT_IF; cmd++) {
+            String name = SftpConstants.getCommandMessageName(cmd);
+            assertFalse("Mismatched name for " + cmd + ": " + name, name.startsWith("SSH_FXP_REALPATH_"));
+        }
+    }
+
+    @Test
+    public void testSubstatusNameResolution() {
+        for (int status = SftpConstants.SSH_FX_OK; status <= SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK; status++) {
+            String name = SftpConstants.getStatusName(status);
+            assertTrue("Failed to convert status=" + status + ": " + name, name.startsWith("SSH_FX_"));
+        }
+    }
+
+    @Test
+    public void testSubstatusMessageResolution() {
+        for (int status = SftpConstants.SSH_FX_OK; status <= SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK; status++) {
+            String message = SftpHelper.resolveStatusMessage(status);
+            assertTrue("Missing message for status=" + status, GenericUtils.isNotEmpty(message));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroupTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroupTest.java b/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroupTest.java
new file mode 100644
index 0000000..704aa05
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroupTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class SftpUniversalOwnerAndGroupTest extends BaseTestSupport {
+    public SftpUniversalOwnerAndGroupTest() {
+        super();
+    }
+
+    @Test
+    public void testNameFormat() {
+        for (SftpUniversalOwnerAndGroup value : SftpUniversalOwnerAndGroup.VALUES) {
+            String name = value.getName();
+            assertFalse(value.name() + ": empty name", GenericUtils.isEmpty(name));
+            assertTrue(value.name() + ": bad suffix", name.charAt(name.length() - 1) == '@');
+
+            for (int index = 0; index < name.length() - 1; index++) {
+                char ch = name.charAt(index);
+                if ((ch < 'A') || (ch > 'Z')) {
+                    fail("Non-uppercase character in " + name);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testFromName() {
+        for (String name : new String[]{null, "", getCurrentTestName()}) {
+            assertNull("Unexpected value for '" + name + "'", SftpUniversalOwnerAndGroup.fromName(name));
+        }
+
+        for (SftpUniversalOwnerAndGroup expected : SftpUniversalOwnerAndGroup.VALUES) {
+            String name = expected.getName();
+            for (int index = 0; index < name.length(); index++) {
+                assertSame(name, expected, SftpUniversalOwnerAndGroup.fromName(name));
+                name = shuffleCase(name);
+            }
+        }
+    }
+}