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/11/02 06:25:52 UTC

[2/3] mina-sshd git commit: [SSHD-572] Invalid handling of set time values for SFTP version 3

[SSHD-572] Invalid handling of set time values for SFTP version 3


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

Branch: refs/heads/master
Commit: beffd2aae2bd029aafe56f4d300ad3098907a46f
Parents: b70ce99
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Nov 2 07:20:15 2015 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Nov 2 07:20:15 2015 +0200

----------------------------------------------------------------------
 .../sshd/client/session/ClientSession.java      |  17 +
 .../sshd/client/session/ClientSessionImpl.java  |  17 +-
 .../subsystem/sftp/AbstractSftpClient.java      | 173 +++---
 .../subsystem/sftp/DefaultSftpClient.java       |   3 +-
 .../sshd/client/subsystem/sftp/SftpClient.java  | 174 ++++--
 .../sshd/client/subsystem/sftp/SftpCommand.java |   4 +-
 .../client/subsystem/sftp/SftpFileChannel.java  |   2 +-
 .../client/subsystem/sftp/SftpFileSystem.java   |  20 +-
 .../subsystem/sftp/SftpFileSystemProvider.java  |  58 +-
 .../subsystem/sftp/SftpPosixFileAttributes.java |  19 +-
 .../subsystem/sftp/SftpVersionSelector.java     | 122 ++++
 .../sshd/common/subsystem/sftp/SftpHelper.java  | 551 +++++++++++++++++++
 .../apache/sshd/common/util/GenericUtils.java   |  34 ++
 .../apache/sshd/common/util/ValidateUtils.java  |  26 +-
 .../sshd/server/subsystem/sftp/SftpHelper.java  | 546 ------------------
 .../server/subsystem/sftp/SftpSubsystem.java    |  10 +-
 .../client/simple/SimpleSftpClientTest.java     |   2 +-
 .../subsystem/sftp/SftpFileSystemTest.java      |  19 +-
 .../sshd/client/subsystem/sftp/SftpTest.java    |   3 +-
 .../subsystem/sftp/SftpVersionSelectorTest.java | 122 ++++
 .../client/subsystem/sftp/SftpVersionsTest.java | 143 +++++
 .../org/apache/sshd/util/test/JSchLogger.java   |   2 +-
 sshd-core/src/test/resources/log4j.properties   |  12 +-
 sshd-git/src/test/resources/log4j.properties    |  12 +-
 24 files changed, 1354 insertions(+), 737 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index a217630..87e50f5 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -231,6 +231,19 @@ public interface ClientSession extends Session, KeyPairProviderHolder {
     SftpClient createSftpClient() throws IOException;
 
     /**
+     * Creates an SFTP client using the specified version
+     *
+     * @param version The version to use - <B>Note:</B> if the specified
+     *                version is not supported by the server then an exception
+     *                will occur
+     * @return The created {@link SftpClient}
+     * @throws IOException If failed to create the client or use the specified version
+     */
+    SftpClient createSftpClient(int version) throws IOException;
+
+    /**
+     * Creates an SFTP client while allowing the selection of a specific version
+     *
      * @param selector The {@link SftpVersionSelector} to use - <B>Note:</B>
      *                 if the server does not support versions re-negotiation then the
      *                 selector will be presented with only one &quot;choice&quot; - the
@@ -242,10 +255,14 @@ public interface ClientSession extends Session, KeyPairProviderHolder {
 
     FileSystem createSftpFileSystem() throws IOException;
 
+    FileSystem createSftpFileSystem(int version) throws IOException;
+
     FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException;
 
     FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException;
 
+    FileSystem createSftpFileSystem(int version, int readBufferSize, int writeBufferSize) throws IOException;
+
     FileSystem createSftpFileSystem(SftpVersionSelector selector, int readBufferSize, int writeBufferSize) throws IOException;
 
     /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index ff95265..ce597ec 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -442,11 +442,16 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
         return new DefaultScpClient(this, listener);
     }
 
-    @Override
+    @Override   // TODO make this a default method in JDK-8
     public SftpClient createSftpClient() throws IOException {
         return createSftpClient(SftpVersionSelector.CURRENT);
     }
 
+    @Override   // TODO make this a default method in JDK-8
+    public SftpClient createSftpClient(final int version) throws IOException {
+        return createSftpClient(SftpVersionSelector.Utils.fixedVersionSelector(version));
+    }
+
     @Override
     public SftpClient createSftpClient(SftpVersionSelector selector) throws IOException {
         DefaultSftpClient client = new DefaultSftpClient(this);
@@ -460,11 +465,21 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
     }
 
     @Override
+    public FileSystem createSftpFileSystem(int version) throws IOException {
+        return createSftpFileSystem(SftpVersionSelector.Utils.fixedVersionSelector(version));
+    }
+
+    @Override
     public FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException {
         return createSftpFileSystem(selector, SftpClient.DEFAULT_READ_BUFFER_SIZE, SftpClient.DEFAULT_WRITE_BUFFER_SIZE);
     }
 
     @Override
+    public FileSystem createSftpFileSystem(int version, int readBufferSize, int writeBufferSize) throws IOException {
+        return createSftpFileSystem(SftpVersionSelector.Utils.fixedVersionSelector(version), readBufferSize, writeBufferSize);
+    }
+
+    @Override
     public FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException {
         return createSftpFileSystem(SftpVersionSelector.CURRENT, readBufferSize, writeBufferSize);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
index 3655571..bb72962 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
@@ -28,7 +28,6 @@ import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
@@ -36,6 +35,7 @@ import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtensionFactory;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpHelper;
 import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -60,7 +60,6 @@ import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_A
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_SIZE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_SUBSECOND_TIMES;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_REGULAR;
@@ -441,67 +440,55 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
         int version = getVersion();
         if (version == SFTP_V3) {
             if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                attrs.flags.add(Attribute.Size);
-                attrs.size = buffer.getLong();
+                attrs.setSize(buffer.getLong());
             }
             if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                attrs.flags.add(Attribute.UidGid);
-                attrs.uid = buffer.getInt();
-                attrs.gid = buffer.getInt();
+                attrs.owner(buffer.getInt(), buffer.getInt());
             }
             if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                attrs.flags.add(Attribute.Perms);
-                attrs.perms = buffer.getInt();
+                attrs.setPermissions(buffer.getInt());
             }
             if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                attrs.flags.add(Attribute.AcModTime);
-                attrs.atime = buffer.getInt();
-                attrs.mtime = buffer.getInt();
+                attrs.setAccessTime(readTime(buffer, flags));
+                attrs.setModifyTime(readTime(buffer, flags));
             }
         } else if (version >= SFTP_V4) {
             attrs.type = buffer.getUByte();
             if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                attrs.flags.add(Attribute.Size);
-                attrs.size = buffer.getLong();
+                attrs.setSize(buffer.getLong());
             }
             if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                attrs.flags.add(Attribute.OwnerGroup);
-                attrs.owner = buffer.getString();
-                attrs.group = buffer.getString();
+                attrs.setOwner(buffer.getString());
+                attrs.setGroup(buffer.getString());
             }
             if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                attrs.flags.add(Attribute.Perms);
-                attrs.perms = buffer.getInt();
+                attrs.setPermissions(buffer.getInt());
             }
 
             // update the permissions according to the type
+            int perms = attrs.getPermissions();
             switch (attrs.type) {
                 case SSH_FILEXFER_TYPE_REGULAR:
-                    attrs.perms |= S_IFREG;
+                    perms |= S_IFREG;
                     break;
                 case SSH_FILEXFER_TYPE_DIRECTORY:
-                    attrs.perms |= S_IFDIR;
+                    perms |= S_IFDIR;
                     break;
                 case SSH_FILEXFER_TYPE_SYMLINK:
-                    attrs.perms |= S_IFLNK;
+                    perms |= S_IFLNK;
                     break;
                 default:    // do nothing
             }
+            attrs.setPermissions(perms);
 
             if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                attrs.flags.add(Attribute.AccessTime);
-                attrs.accessTime = readTime(buffer, flags);
-                attrs.atime = (int) attrs.accessTime.to(TimeUnit.SECONDS);
+                attrs.setAccessTime(readTime(buffer, flags));
             }
             if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                attrs.flags.add(Attribute.CreateTime);
-                attrs.createTime = readTime(buffer, flags);
-                attrs.ctime = (int) attrs.createTime.to(TimeUnit.SECONDS);
+                attrs.setCreateTime(readTime(buffer, flags));
             }
             if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                attrs.flags.add(Attribute.ModifyTime);
-                attrs.modifyTime = readTime(buffer, flags);
-                attrs.mtime = (int) attrs.modifyTime.to(TimeUnit.SECONDS);
+                attrs.setModifyTime(readTime(buffer, flags));
             }
             // TODO: acl
         } else {
@@ -511,12 +498,7 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
     }
 
     protected FileTime readTime(Buffer buffer, int flags) {
-        long secs = buffer.getLong();
-        long millis = secs * 1000;
-        if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-            millis += buffer.getInt() / 1000000L;
-        }
-        return FileTime.from(millis, TimeUnit.MILLISECONDS);
+        return SftpHelper.readTime(buffer, getVersion(), flags);
     }
 
     protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
@@ -534,26 +516,34 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
                     case Perms:
                         flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
                         break;
-                    case AcModTime:
-                        flags |= SSH_FILEXFER_ATTR_ACMODTIME;
+                    case AccessTime:
+                        if (attributes.flags.contains(Attribute.ModifyTime)) {
+                            flags |= SSH_FILEXFER_ATTR_ACMODTIME;
+                        }
+                        break;
+                    case ModifyTime:
+                        if (attributes.flags.contains(Attribute.AccessTime)) {
+                            flags |= SSH_FILEXFER_ATTR_ACMODTIME;
+                        }
                         break;
                     default:    // do nothing
                 }
             }
             buffer.putInt(flags);
             if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(attributes.size);
+                buffer.putLong(attributes.getSize());
             }
             if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                buffer.putInt(attributes.uid);
-                buffer.putInt(attributes.gid);
+                buffer.putInt(attributes.getUserId());
+                buffer.putInt(attributes.getGroupId());
             }
             if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributes.perms);
+                buffer.putInt(attributes.getPermissions());
             }
+
             if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                buffer.putInt(attributes.atime);
-                buffer.putInt(attributes.mtime);
+                SftpHelper.writeTime(buffer, version, flags, attributes.getAccessTime());
+                SftpHelper.writeTime(buffer, version, flags, attributes.getModifyTime());
             }
         } else if (version >= SFTP_V4) {
             int flags = 0;
@@ -583,42 +573,28 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             buffer.putInt(flags);
             buffer.putByte((byte) attributes.type);
             if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(attributes.size);
+                buffer.putLong(attributes.getSize());
             }
             if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                buffer.putString(attributes.owner != null ? attributes.owner : "OWNER@");
-                buffer.putString(attributes.group != null ? attributes.group : "GROUP@");
+                String owner = attributes.getOwner();
+                buffer.putString(GenericUtils.isEmpty(owner) ? "OWNER@" : owner);
+
+                String group = attributes.getGroup();
+                buffer.putString(GenericUtils.isEmpty(group) ? "GROUP@" : group);
             }
             if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributes.perms);
+                buffer.putInt(attributes.getPermissions());
             }
             if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                buffer.putLong(attributes.accessTime.to(TimeUnit.SECONDS));
-                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-                    long nanos = attributes.accessTime.to(TimeUnit.NANOSECONDS);
-                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-                    buffer.putInt((int) nanos);
-                }
-                buffer.putInt(attributes.atime);
+                SftpHelper.writeTime(buffer, version, flags, attributes.getAccessTime());
             }
             if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                buffer.putLong(attributes.createTime.to(TimeUnit.SECONDS));
-                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-                    long nanos = attributes.createTime.to(TimeUnit.NANOSECONDS);
-                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-                    buffer.putInt((int) nanos);
-                }
-                buffer.putInt(attributes.atime);
+                SftpHelper.writeTime(buffer, version, flags, attributes.getCreateTime());
             }
             if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                buffer.putLong(attributes.modifyTime.to(TimeUnit.SECONDS));
-                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-                    long nanos = attributes.modifyTime.to(TimeUnit.NANOSECONDS);
-                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-                    buffer.putInt((int) nanos);
-                }
-                buffer.putInt(attributes.atime);
+                SftpHelper.writeTime(buffer, version, flags, attributes.getModifyTime());
             }
+            // TODO: for v6+ add CTIME (see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-21)
             // TODO: acl
         } else {
             throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version);
@@ -695,7 +671,12 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
         }
         buffer.putInt(mode);
         writeAttributes(buffer, new Attributes());
-        return new DefaultCloseableHandle(this, checkHandle(SSH_FXP_OPEN, buffer));
+
+        CloseableHandle handle = new DefaultCloseableHandle(this, checkHandle(SSH_FXP_OPEN, buffer));
+        if (log.isTraceEnabled()) {
+            log.trace("open({})[{}] options={}: {}", getClientSession(), path, options, handle);
+        }
+        return handle;
     }
 
     @Override
@@ -704,7 +685,11 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("close(" + handle + ") client is closed");
         }
 
-        byte[] id = handle.getIdentifier();
+        if (log.isTraceEnabled()) {
+            log.trace("close({}) {}", getClientSession(), handle);
+        }
+
+        byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
         Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */);
         buffer.putBytes(id);
         checkStatus(SSH_FXP_CLOSE, buffer);
@@ -716,6 +701,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("remove(" + path + ") client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("remove({}) {}", getClientSession(), path);
+        }
+
         Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
         checkStatus(SSH_FXP_REMOVE, buffer);
@@ -727,6 +716,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("rename(" + oldPath + " => " + newPath + ")[" + options + "] client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("rename({}) {} => {}", getClientSession(), oldPath, newPath);
+        }
+
         Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */);
         buffer.putString(oldPath);
         buffer.putString(newPath);
@@ -835,6 +828,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("mkdir(" + path + ") client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("mkdir({}) {}", getClientSession(), path);
+        }
+
         Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
         buffer.putInt(0);
@@ -853,6 +850,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("rmdir(" + path + ") client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("rmdir({}) {}", getClientSession(), path);
+        }
+
         Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
         checkStatus(SSH_FXP_RMDIR, buffer);
@@ -866,7 +867,13 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
 
         Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
-        return new DefaultCloseableHandle(this, checkHandle(SSH_FXP_OPENDIR, buffer));
+
+        CloseableHandle handle = new DefaultCloseableHandle(this, checkHandle(SSH_FXP_OPENDIR, buffer));
+        if (log.isTraceEnabled()) {
+            log.trace("openDir({})[{}}: {}", getClientSession(), path, handle);
+        }
+
+        return handle;
     }
 
     @Override
@@ -989,6 +996,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("setStat(" + path + ")[" + attributes + "] client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("setStat({})[{}]: {}", getClientSession(), path, attributes);
+        }
+
         Buffer buffer = new ByteArrayBuffer();
         buffer.putString(path);
         writeAttributes(buffer, attributes);
@@ -1001,6 +1012,9 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("setStat(" + handle + ")[" + attributes + "] client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("setStat({})[{}]: {}", getClientSession(), handle, attributes);
+        }
         byte[] id = handle.getIdentifier();
         Buffer buffer = new ByteArrayBuffer(id.length + (2 * Long.SIZE) /* some extras */);
         buffer.putBytes(id);
@@ -1025,6 +1039,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("link(" + linkPath + " => " + targetPath + ")[symbolic=" + symbolic + "] client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("link({})[symbolic={}] {} => {}", getClientSession(), symbolic, linkPath, targetPath);
+        }
+
         Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */);
         int version = getVersion();
         if (version < SFTP_V6) {
@@ -1048,6 +1066,11 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("lock(" + handle + ")[offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask) + "] client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("lock({})[{}] offset={}, length={}, mask=0x{}",
+                      getClientSession(), handle, offset, length, Integer.toHexString(mask));
+        }
+
         byte[] id = handle.getIdentifier();
         Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */);
         buffer.putBytes(id);
@@ -1063,6 +1086,10 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new IOException("unlock" + handle + ")[offset=" + offset + ", length=" + length + "] client is closed");
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("unlock({})[{}] offset={}, length={}", getClientSession(), handle, offset, length);
+        }
+
         byte[] id = handle.getIdentifier();
         Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */);
         buffer.putBytes(id);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
index aa8aead..8d2c057 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
@@ -219,8 +219,7 @@ public class DefaultSftpClient extends AbstractSftpClient {
         int id = cmdId.incrementAndGet();
         int len = buffer.available();
         if (log.isTraceEnabled()) {
-            log.trace("send(cmd={}, len={}) id = {}",
-                    cmd, len, id);
+            log.trace("send(cmd={}, len={}) id={}", cmd, len, id);
         }
 
         OutputStream dos = channel.getInvertedIn();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index 538b198..652b3d7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -66,18 +66,17 @@ public interface SftpClient extends SubsystemClient {
         Size,
         UidGid,
         Perms,
-        AcModTime,
         OwnerGroup,
         AccessTime,
         ModifyTime,
-        CreateTime,
+        CreateTime
     }
 
     class Handle {
         private final byte[] id;
 
         Handle(byte[] id) {
-            // clone the original so the handle is imutable
+            // clone the original so the handle is immutable
             this.id = ValidateUtils.checkNotNullAndNotEmpty(id, "No handle ID").clone();
         }
 
@@ -133,58 +132,92 @@ public interface SftpClient extends SubsystemClient {
     class Attributes {
         // CHECKSTYLE:OFF
         public final Set<Attribute> flags = EnumSet.noneOf(Attribute.class);
-        public long size;
         public int type;
-        public int uid;
-        public int gid;
-        public int perms;
-        public int atime;
-        public int ctime;
-        public int mtime;
-        public String owner;
-        public String group;
-        public FileTime accessTime;
-        public FileTime createTime;
-        public FileTime modifyTime;
         // CHECKSTYLE:ON
 
+        private int perms;
+        private int uid;
+        private int gid;
+        private String owner;
+        private String group;
+        private long size;
+        private FileTime accessTime;
+        private FileTime createTime;
+        private FileTime modifyTime;
+
+        public Attributes() {
+            super();
+        }
+
         @Override
         public String toString() {
             return "type=" + type
-                    + ";size=" + size
-                    + ";uid=" + uid
-                    + ";gid=" + gid
-                    + ";perms=0x" + Integer.toHexString(perms)
-                    + ";flags=" + flags
-                    + ";owner=" + owner
-                    + ";group=" + group
-                    + ";aTime=(" + atime + ")[" + accessTime + "]"
-                    + ";cTime=(" + ctime + ")[" + createTime + "]"
-                    + ";mTime=(" + mtime + ")[" + modifyTime + "]";
+                 + ";size=" + getSize()
+                 + ";uid=" + getUserId()
+                 + ";gid=" + getGroupId()
+                 + ";perms=0x" + Integer.toHexString(getPermissions())
+                 + ";flags=" + flags
+                 + ";owner=" + getOwner()
+                 + ";group=" + getGroup()
+                 + ";aTime=" + getAccessTime()
+                 + ";cTime=" + getCreateTime()
+                 + ";mTime=" + getModifyTime();
+        }
+
+        public long getSize() {
+            return size;
         }
 
         public Attributes size(long size) {
+            setSize(size);
+            return this;
+        }
+
+        public void setSize(long size) {
             flags.add(Attribute.Size);
             this.size = size;
-            return this;
+        }
+
+        public String getOwner() {
+            return owner;
         }
 
         public Attributes owner(String owner) {
+            setOwner(owner);
+            return this;
+        }
+
+        public void setOwner(String owner) {
             flags.add(Attribute.OwnerGroup);
             this.owner = owner;
             if (GenericUtils.isEmpty(group)) {
                 group = "GROUP@";
             }
-            return this;
+        }
+
+        public String getGroup() {
+            return group;
         }
 
         public Attributes group(String group) {
+            setGroup(group);
+            return this;
+        }
+
+        public void setGroup(String group) {
             flags.add(Attribute.OwnerGroup);
             this.group = group;
             if (GenericUtils.isEmpty(owner)) {
                 owner = "OWNER@";
             }
-            return this;
+        }
+
+        public int getUserId() {
+            return uid;
+        }
+
+        public int getGroupId() {
+            return gid;
         }
 
         public Attributes owner(int uid, int gid) {
@@ -194,61 +227,86 @@ public interface SftpClient extends SubsystemClient {
             return this;
         }
 
+        public int getPermissions() {
+            return perms;
+        }
+
         public Attributes perms(int perms) {
+            setPermissions(perms);
+            return this;
+        }
+
+        public void setPermissions(int perms) {
             flags.add(Attribute.Perms);
             this.perms = perms;
-            return this;
         }
 
-        public Attributes atime(int atime) {
-            flags.add(Attribute.AccessTime);
-            this.atime = atime;
-            this.accessTime = FileTime.from(atime, TimeUnit.SECONDS);
-            return this;
+        public FileTime getAccessTime() {
+            return accessTime;
         }
 
-        public Attributes ctime(int ctime) {
-            flags.add(Attribute.CreateTime);
-            this.ctime = ctime;
-            this.createTime = FileTime.from(atime, TimeUnit.SECONDS);
-            return this;
+        public Attributes accessTime(long atime) {
+            return accessTime(atime, TimeUnit.SECONDS);
         }
 
-        public Attributes mtime(int mtime) {
-            flags.add(Attribute.ModifyTime);
-            this.mtime = mtime;
-            this.modifyTime = FileTime.from(atime, TimeUnit.SECONDS);
-            return this;
+        public Attributes accessTime(long atime, TimeUnit unit) {
+            return accessTime(FileTime.from(atime, unit));
         }
 
-        public Attributes time(int atime, int mtime) {
-            flags.add(Attribute.AcModTime);
-            this.atime = atime;
-            this.mtime = mtime;
+        public Attributes accessTime(FileTime atime) {
+            setAccessTime(atime);
             return this;
         }
 
-        public Attributes accessTime(FileTime atime) {
+        public void setAccessTime(FileTime atime) {
             flags.add(Attribute.AccessTime);
-            this.atime = (int) atime.to(TimeUnit.SECONDS);
-            this.accessTime = atime;
-            return this;
+            accessTime = ValidateUtils.checkNotNull(atime, "No access time");
+        }
+
+        public FileTime getCreateTime() {
+            return createTime;
+        }
+
+        public Attributes createTime(long ctime) {
+            return createTime(ctime, TimeUnit.SECONDS);
+        }
+
+        public Attributes createTime(long ctime, TimeUnit unit) {
+            return createTime(FileTime.from(ctime, unit));
         }
 
         public Attributes createTime(FileTime ctime) {
-            flags.add(Attribute.CreateTime);
-            this.ctime = (int) ctime.to(TimeUnit.SECONDS);
-            this.createTime = ctime;
+            setCreateTime(ctime);
             return this;
         }
 
+        public void setCreateTime(FileTime ctime) {
+            flags.add(Attribute.CreateTime);
+            createTime = ValidateUtils.checkNotNull(ctime, "No create time");
+        }
+
+        public FileTime getModifyTime() {
+            return modifyTime;
+        }
+
+        public Attributes modifyTime(long mtime) {
+            return modifyTime(mtime, TimeUnit.SECONDS);
+        }
+
+        public Attributes modifyTime(long mtime, TimeUnit unit) {
+            return modifyTime(FileTime.from(mtime, unit));
+        }
+
         public Attributes modifyTime(FileTime mtime) {
-            flags.add(Attribute.ModifyTime);
-            this.mtime = (int) mtime.to(TimeUnit.SECONDS);
-            this.modifyTime = mtime;
+            setModifyTime(mtime);
             return this;
         }
 
+        public void setModifyTime(FileTime mtime) {
+            flags.add(Attribute.ModifyTime);
+            modifyTime = ValidateUtils.checkNotNull(mtime, "No modify time");
+        }
+
         public boolean isRegularFile() {
             return (perms & S_IFMT) == S_IFREG;
         }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
index a00e05d..e07e3ed 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
@@ -145,8 +145,8 @@ public class SftpCommand implements Channel {
     }
 
     protected <A extends Appendable> A appendFileAttributes(A stdout, SftpClient sftp, String path, Attributes attrs) throws IOException {
-        stdout.append('\t').append(Long.toString(attrs.size))
-              .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.perms));
+        stdout.append('\t').append(Long.toString(attrs.getSize()))
+              .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions()));
         if (attrs.isSymbolicLink()) {
             String linkValue = sftp.readLink(path);
             stdout.append(" => ")

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileChannel.java
index bf69719..b656582 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileChannel.java
@@ -217,7 +217,7 @@ public class SftpFileChannel extends FileChannel {
     @Override
     public long size() throws IOException {
         ensureOpen(Collections.<SftpClient.OpenMode>emptySet());
-        return sftp.stat(handle).size;
+        return sftp.stat(handle).getSize();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
index a348a98..cf91f90 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
@@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.session.ClientSessionHolder;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.file.util.BaseFileSystem;
 import org.apache.sshd.common.file.util.ImmutableList;
@@ -47,7 +48,7 @@ import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 
-public class SftpFileSystem extends BaseFileSystem<SftpPath> {
+public class SftpFileSystem extends BaseFileSystem<SftpPath> implements ClientSessionHolder {
     public static final String POOL_SIZE_PROP = "sftp-fs-pool-size";
     public static final int DEFAULT_POOL_SIZE = 8;
 
@@ -59,7 +60,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
                             )));
 
     private final String id;
-    private final ClientSession session;
+    private final ClientSession clientSession;
     private final SftpVersionSelector selector;
     private final Queue<SftpClient> pool;
     private final ThreadLocal<Wrapper> wrappers = new ThreadLocal<>();
@@ -71,7 +72,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
     public SftpFileSystem(SftpFileSystemProvider provider, String id, ClientSession session, SftpVersionSelector selector) throws IOException {
         super(provider);
         this.id = id;
-        this.session = session;
+        this.clientSession = ValidateUtils.checkNotNull(session, "No client session");
         this.selector = ValidateUtils.checkNotNull(selector, "No SFTP version selector provided");
         this.stores = Collections.unmodifiableList(Collections.<FileStore>singletonList(new SftpFileStore(id, this)));
         this.pool = new LinkedBlockingQueue<>(PropertyResolverUtils.getIntProperty(session, POOL_SIZE_PROP, DEFAULT_POOL_SIZE));
@@ -127,8 +128,9 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         return new SftpPath(this, root, names);
     }
 
-    public ClientSession getSession() {
-        return session;
+    @Override
+    public ClientSession getClientSession() {
+        return clientSession;
     }
 
     @SuppressWarnings("synthetic-access")
@@ -138,6 +140,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
             while (wrapper == null) {
                 SftpClient client = pool.poll();
                 if (client == null) {
+                    ClientSession session = getClientSession();
                     client = session.createSftpClient(getSftpVersionSelector());
                 }
                 if (!client.isClosing()) {
@@ -157,6 +160,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
             SftpFileSystemProvider provider = provider();
             String fsId = getId();
             SftpFileSystem fs = provider.removeFileSystem(fsId);
+            ClientSession session = getClientSession();
             session.close(true);
 
             if ((fs != null) && (fs != this)) {
@@ -167,6 +171,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
 
     @Override
     public boolean isOpen() {
+        ClientSession session = getClientSession();
         return session.isOpen();
     }
 
@@ -185,6 +190,11 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         return defaultDir;
     }
 
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + String.valueOf(getClientSession()) + "]";
+    }
+
     private final class Wrapper extends AbstractSftpClient {
 
         private final SftpClient delegate;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
index ad00511..dd6e9a4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
@@ -200,6 +200,9 @@ public class SftpFileSystemProvider extends FileSystemProvider {
 
         fileSystem.setReadBufferSize(PropertyResolverUtils.getIntProperty(resolver, READ_BUFFER_PROP_NAME, DEFAULT_READ_BUFFER_SIZE));
         fileSystem.setWriteBufferSize(PropertyResolverUtils.getIntProperty(resolver, WRITE_BUFFER_PROP_NAME, DEFAULT_WRITE_BUFFER_SIZE));
+        if (log.isDebugEnabled()) {
+            log.debug("newFileSystem({}): {}", uri.toASCIIString(), fileSystem);
+        }
         return fileSystem;
     }
 
@@ -216,6 +219,10 @@ public class SftpFileSystemProvider extends FileSystemProvider {
 
         fileSystem.setReadBufferSize(PropertyResolverUtils.getIntProperty(session, READ_BUFFER_PROP_NAME, DEFAULT_READ_BUFFER_SIZE));
         fileSystem.setWriteBufferSize(PropertyResolverUtils.getIntProperty(session, WRITE_BUFFER_PROP_NAME, DEFAULT_WRITE_BUFFER_SIZE));
+        if (log.isDebugEnabled()) {
+            log.debug("newFileSystem: {}", fileSystem);
+        }
+
         return fileSystem;
     }
 
@@ -238,9 +245,15 @@ public class SftpFileSystemProvider extends FileSystemProvider {
             return null;
         }
 
+        SftpFileSystem removed;
         synchronized (fileSystems) {
-            return fileSystems.remove(id);
+            removed = fileSystems.remove(id);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("removeFileSystem({}): {}", id, removed);
         }
+        return removed;
     }
 
     /**
@@ -315,6 +328,9 @@ public class SftpFileSystemProvider extends FileSystemProvider {
     public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
         SftpPath p = toSftpPath(dir);
         SftpFileSystem fs = p.getFileSystem();
+        if (log.isDebugEnabled()) {
+            log.debug("createDirectory({}) {} ({})", fs, dir, Arrays.asList(attrs));
+        }
         try (SftpClient sftp = fs.getClient()) {
             try {
                 sftp.mkdir(dir.toString());
@@ -347,6 +363,10 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         checkAccess(p, AccessMode.WRITE);
 
         SftpFileSystem fs = p.getFileSystem();
+        if (log.isDebugEnabled()) {
+            log.debug("delete({}) {}", fs, path);
+        }
+
         try (SftpClient sftp = fs.getClient()) {
             BasicFileAttributes attributes = readAttributes(path, BasicFileAttributes.class);
             if (attributes.isDirectory()) {
@@ -388,6 +408,10 @@ public class SftpFileSystemProvider extends FileSystemProvider {
             throw new AccessDeniedException("Existence cannot be determined for copy target: " + target);
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("copy({})[{}] {} => {}", src.getFileSystem(), Arrays.asList(options), src, dst);
+        }
+
         if (replaceExisting) {
             deleteIfExists(target);
         } else {
@@ -447,7 +471,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         // attributes of source file
         BasicFileAttributes attrs = readAttributes(source, BasicFileAttributes.class, linkOptions);
         if (attrs.isSymbolicLink()) {
-            throw new IOException("Moving of source symbolic link not supported: " + source);
+            throw new IOException("Moving of source symbolic link (" + source + ") to " + target + " not supported");
         }
 
         // delete target if it exists and REPLACE_EXISTING is specified
@@ -456,6 +480,10 @@ public class SftpFileSystemProvider extends FileSystemProvider {
             throw new AccessDeniedException("Existence cannot be determined for move target " + target);
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("move({})[{}] {} => {}", src.getFileSystem(), Arrays.asList(options), src, dst);
+        }
+
         if (replaceExisting) {
             deleteIfExists(target);
         } else if (status) {
@@ -525,6 +553,11 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         if (fsLink != t.getFileSystem()) {
             throw new ProviderMismatchException("Mismatched file system providers for " + l + " vs. " + t);
         }
+
+        if (log.isDebugEnabled()) {
+            log.debug("createSymbolicLink({})[{}] {} => {}", fsLink, Arrays.asList(attrs), link, target);
+        }
+
         try (SftpClient client = fsLink.getClient()) {
             client.symLink(l.toString(), t.toString());
         }
@@ -535,7 +568,12 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         SftpPath l = toSftpPath(link);
         SftpFileSystem fsLink = l.getFileSystem();
         try (SftpClient client = fsLink.getClient()) {
-            return fsLink.getPath(client.readLink(l.toString()));
+            String linkPath = client.readLink(l.toString());
+            if (log.isDebugEnabled()) {
+                log.debug("readSymbolicLink({})[{}] {} => {}", fsLink, link, linkPath);
+            }
+
+            return fsLink.getPath(linkPath);
         }
     }
 
@@ -658,7 +696,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
                     break;
                 default:
                     if (log.isTraceEnabled()) {
-                        log.trace("readAttributes({})[{}] ignored {}={}", path, attributes, attr, v);
+                        log.trace("readAttributes({})[{}] ignored {}={} for {}", fs, path, attr, v, attributes);
                     }
             }
         }
@@ -687,13 +725,13 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         SftpClient.Attributes attributes = new SftpClient.Attributes();
         switch (attr) {
             case "lastModifiedTime":
-                attributes.mtime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+                attributes.modifyTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
                 break;
             case "lastAccessTime":
-                attributes.atime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+                attributes.accessTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
                 break;
             case "creationTime":
-                attributes.ctime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+                attributes.createTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
                 break;
             case "size":
                 attributes.size(((Number) value).longValue());
@@ -718,10 +756,14 @@ public class SftpFileSystemProvider extends FileSystemProvider {
                         + " unknown view=" + view + " attribute: " + attr);
             default:
                 if (log.isTraceEnabled()) {
-                    log.trace("setAttribute({})[{}] ignore {}={}", path, attribute, attr, value);
+                    log.trace("setAttribute({})[{}] ignore {}/{}={}", fs, path, attribute, attr, value);
                 }
         }
 
+        if (log.isDebugEnabled()) {
+            log.debug("setAttribute({}) {}: {}", fs, path, attributes);
+        }
+
         try (SftpClient client = fs.getClient()) {
             client.setStat(p.toString(), attributes);
         }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributes.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributes.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributes.java
index 9b0e0d3..b510907 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributes.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributes.java
@@ -25,9 +25,8 @@ import java.nio.file.attribute.PosixFileAttributes;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.UserPrincipal;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
 import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.common.util.GenericUtils;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -50,32 +49,34 @@ public class SftpPosixFileAttributes implements PosixFileAttributes {
 
     @Override
     public UserPrincipal owner() {
-        return attributes.owner != null ? new SftpFileSystem.DefaultGroupPrincipal(attributes.owner) : null;
+        String owner = attributes.getOwner();
+        return GenericUtils.isEmpty(owner) ? null : new SftpFileSystem.DefaultGroupPrincipal(owner);
     }
 
     @Override
     public GroupPrincipal group() {
-        return attributes.group != null ? new SftpFileSystem.DefaultGroupPrincipal(attributes.group) : null;
+        String group = attributes.getGroup();
+        return GenericUtils.isEmpty(group) ? null : new SftpFileSystem.DefaultGroupPrincipal(group);
     }
 
     @Override
     public Set<PosixFilePermission> permissions() {
-        return SftpFileSystemProvider.permissionsToAttributes(attributes.perms);
+        return SftpFileSystemProvider.permissionsToAttributes(attributes.getPermissions());
     }
 
     @Override
     public FileTime lastModifiedTime() {
-        return FileTime.from(attributes.mtime, TimeUnit.SECONDS);
+        return attributes.getModifyTime();
     }
 
     @Override
     public FileTime lastAccessTime() {
-        return FileTime.from(attributes.atime, TimeUnit.SECONDS);
+        return attributes.getAccessTime();
     }
 
     @Override
     public FileTime creationTime() {
-        return FileTime.from(attributes.ctime, TimeUnit.SECONDS);
+        return attributes.getCreateTime();
     }
 
     @Override
@@ -100,7 +101,7 @@ public class SftpPosixFileAttributes implements PosixFileAttributes {
 
     @Override
     public long size() {
-        return attributes.size;
+        return attributes.getSize();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
index e5f41bf..240950e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
@@ -19,8 +19,12 @@
 
 package org.apache.sshd.client.subsystem.sftp;
 
+import java.util.Collection;
 import java.util.List;
 
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
@@ -37,6 +41,42 @@ public interface SftpVersionSelector {
     };
 
     /**
+     * An {@link SftpVersionSelector} that returns the maximum available version
+     */
+    SftpVersionSelector MAXIMUM = new SftpVersionSelector() {
+        @Override
+        public int selectVersion(int current, List<Integer> available) {
+            int candidate = current;
+            if (GenericUtils.size(available) > 0) {
+                for (Number version : available) {
+                    if (candidate < version.intValue()) {
+                        candidate = version.intValue();
+                    }
+                }
+            }
+            return candidate;
+        }
+    };
+
+    /**
+     * An {@link SftpVersionSelector} that returns the maximum available version
+     */
+    SftpVersionSelector MINIMUM = new SftpVersionSelector() {
+        @Override
+        public int selectVersion(int current, List<Integer> available) {
+            int candidate = current;
+            if (GenericUtils.size(available) > 0) {
+                for (Number version : available) {
+                    if (candidate > version.intValue()) {
+                        candidate = version.intValue();
+                    }
+                }
+            }
+            return candidate;
+        }
+    };
+
+    /**
      * @param current   The current version negotiated with the server
      * @param available Extra versions available - may be empty and/or contain
      *                  only the current one
@@ -44,4 +84,86 @@ public interface SftpVersionSelector {
      */
     int selectVersion(int current, List<Integer> available);
 
+    /**
+     * Utility class to help using {@link SftpVersionSelector}s
+     */
+    // CHECKSTYLE:OFF
+    final class Utils {
+    // CHECKSTYLE:ON
+
+        private Utils() {
+            throw new UnsupportedOperationException("No instance allowed");
+        }
+
+        /**
+         * Creates a selector the always returns the requested (fixed version) regardless
+         * of what the current or reported available versions are. If the requested version
+         * is not reported as available then an exception will be eventually thrown by the
+         * client during re-negotiation phase.
+         *
+         * @param version The requested version
+         * @return The {@link SftpVersionSelector}
+         */
+        public static SftpVersionSelector fixedVersionSelector(final int version) {
+            return new SftpVersionSelector() {
+                @Override
+                public int selectVersion(int current, List<Integer> available) {
+                    return version;
+                }
+            };
+        }
+
+        /**
+         * Selects a version in order of preference - if none of the preferred
+         * versions is listed as available then an exception is thrown when the
+         * {@link SftpVersionSelector#selectVersion(int, List)} method is invoked
+         *
+         * @param preferred The preferred versions in decreasing order of
+         * preference (i.e., most preferred is 1st) - may not be {@code null}/empty
+         * @return A {@link SftpVersionSelector} that attempts to select
+         * the most preferred version that is also listed as available.
+         */
+        public static SftpVersionSelector preferredVersionSelector(final int ... preferred) {
+            return preferredVersionSelector(GenericUtils.asList(preferred));
+
+        }
+
+        /**
+         * Selects a version in order of preference - if none of the preferred
+         * versions is listed as available then an exception is thrown when the
+         * {@link SftpVersionSelector#selectVersion(int, List)} method is invoked
+         *
+         * @param preferred The preferred versions in decreasing order of
+         * preference (i.e., most preferred is 1st)
+         * @return A {@link SftpVersionSelector} that attempts to select
+         * the most preferred version that is also listed as available.
+         */
+        public static SftpVersionSelector preferredVersionSelector(final Iterable<? extends Number> preferred) {
+            if (preferred instanceof Collection<?>) {
+                ValidateUtils.checkNotNullAndNotEmpty((Collection<?>) preferred, "Empty preferred versions");
+            } else {
+                ValidateUtils.checkNotNull(preferred, "No preferred versions");
+            }
+
+            return new SftpVersionSelector() {
+                @Override
+                public int selectVersion(int current, List<Integer> available) {
+                    for (Number prefValue : preferred) {
+                        int version = prefValue.intValue();
+                        if (version == current) {
+                            return version;
+                        }
+
+                        for (Integer avail : available) {
+                            if (version == avail.intValue()) {
+                                return version;
+                            }
+                        }
+                    }
+
+                    throw new IllegalStateException("Preferred versions (" + preferred + ") not available: " + available);
+                }
+            };
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
new file mode 100644
index 0000000..f62274a
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
@@ -0,0 +1,551 @@
+/*
+ * 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 java.io.EOFException;
+import java.io.FileNotFoundException;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+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.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.server.subsystem.sftp.DefaultGroupPrincipal;
+import org.apache.sshd.server.subsystem.sftp.InvalidHandleException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SftpHelper {
+
+    private SftpHelper() {
+        throw new UnsupportedOperationException("No instance allowed");
+    }
+
+    /**
+     * Writes a file / folder's attributes to a buffer
+     *
+     * @param buffer The target {@link Buffer}
+     * @param version The output encoding version
+     * @param attributes The {@link Map} of attributes
+     * @see #writeAttrsV3(Buffer, int, Map)
+     * @see #writeAttrsV4(Buffer, int, Map)
+     */
+    public static void writeAttrs(Buffer buffer, int version, Map<String, ?> attributes) {
+        if (version == SftpConstants.SFTP_V3) {
+            writeAttrsV3(buffer, version, attributes);
+        } else if (version >= SftpConstants.SFTP_V4) {
+            writeAttrsV4(buffer, version, attributes);
+        } else {
+            throw new IllegalStateException("Unsupported SFTP version: " + version);
+        }
+    }
+
+    /**
+     * Writes the retrieved file / directory attributes in V3 format
+     *
+     * @param buffer The target {@link Buffer}
+     * @param version The actual version - must be {@link SftpConstants#SFTP_V3}
+     * @param attributes The {@link Map} of attributes
+     */
+    public static void writeAttrsV3(Buffer buffer, int version, Map<String, ?> attributes) {
+        ValidateUtils.checkTrue(version == SftpConstants.SFTP_V3, "Illegal version: %d", version);
+
+        boolean isReg = getBool((Boolean) attributes.get("isRegularFile"));
+        boolean isDir = getBool((Boolean) attributes.get("isDirectory"));
+        boolean isLnk = getBool((Boolean) attributes.get("isSymbolicLink"));
+        @SuppressWarnings("unchecked")
+        Collection<PosixFilePermission> perms = (Collection<PosixFilePermission>) attributes.get("permissions");
+        Number size = (Number) attributes.get("size");
+        FileTime lastModifiedTime = (FileTime) attributes.get("lastModifiedTime");
+        FileTime lastAccessTime = (FileTime) attributes.get("lastAccessTime");
+
+        int flags = ((isReg || isLnk) && (size != null) ? SftpConstants.SSH_FILEXFER_ATTR_SIZE : 0)
+                  | (attributes.containsKey("uid") && attributes.containsKey("gid") ? SftpConstants.SSH_FILEXFER_ATTR_UIDGID : 0)
+                  | ((perms != null) ? SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS : 0)
+                  | (((lastModifiedTime != null) && (lastAccessTime != null)) ? SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME : 0);
+        buffer.putInt(flags);
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+            buffer.putLong(size.longValue());
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
+            buffer.putInt(((Number) attributes.get("uid")).intValue());
+            buffer.putInt(((Number) attributes.get("gid")).intValue());
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+            buffer.putInt(attributesToPermissions(isReg, isDir, isLnk, perms));
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+            writeTime(buffer, version, flags, lastAccessTime);
+            writeTime(buffer, version, flags, lastModifiedTime);
+        }
+    }
+
+    /**
+     * Writes the retrieved file / directory attributes in V3 format
+     *
+     * @param buffer The target {@link Buffer}
+     * @param version The actual version - must be at least {@link SftpConstants#SFTP_V4}
+     * @param attributes The {@link Map} of attributes
+     */
+    public static void writeAttrsV4(Buffer buffer, int version, Map<String, ?> attributes) {
+        ValidateUtils.checkTrue(version >= SftpConstants.SFTP_V4, "Illegal version: %d", version);
+
+        boolean isReg = getBool((Boolean) attributes.get("isRegularFile"));
+        boolean isDir = getBool((Boolean) attributes.get("isDirectory"));
+        boolean isLnk = getBool((Boolean) attributes.get("isSymbolicLink"));
+        @SuppressWarnings("unchecked")
+        Collection<PosixFilePermission> perms = (Collection<PosixFilePermission>) attributes.get("permissions");
+        Number size = (Number) attributes.get("size");
+        FileTime lastModifiedTime = (FileTime) attributes.get("lastModifiedTime");
+        FileTime lastAccessTime = (FileTime) attributes.get("lastAccessTime");
+        FileTime creationTime = (FileTime) attributes.get("creationTime");
+        int flags = (((isReg || isLnk) && (size != null)) ? SftpConstants.SSH_FILEXFER_ATTR_SIZE : 0)
+                  | ((attributes.containsKey("owner") && attributes.containsKey("group")) ? SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP : 0)
+                  | ((perms != null) ? SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS : 0)
+                  | ((lastModifiedTime != null) ? SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME : 0)
+                  | ((creationTime != null) ? SftpConstants.SSH_FILEXFER_ATTR_CREATETIME : 0)
+                  | ((lastAccessTime != null) ? SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME : 0);
+        buffer.putInt(flags);
+        buffer.putByte((byte) (isReg ? SftpConstants.SSH_FILEXFER_TYPE_REGULAR
+                : isDir ? SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY
+                : isLnk ? SftpConstants.SSH_FILEXFER_TYPE_SYMLINK
+                : SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN));
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+            buffer.putLong(size.longValue());
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+            buffer.putString(Objects.toString(attributes.get("owner"), null));
+            buffer.putString(Objects.toString(attributes.get("group"), null));
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+            buffer.putInt(attributesToPermissions(isReg, isDir, isLnk, perms));
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+            writeTime(buffer, version, flags, lastAccessTime);
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+            writeTime(buffer, version, flags, lastAccessTime);
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+            writeTime(buffer, version, flags, lastModifiedTime);
+        }
+        // TODO: acls
+        // TODO: bits
+        // TODO: extended
+    }
+
+    /**
+     * @param bool The {@link Boolean} value
+     * @return {@code true} it the argument is non-{@code null} and
+     * its {@link Boolean#booleanValue()} is {@code true}
+     */
+    public static boolean getBool(Boolean bool) {
+        return bool != null && bool;
+    }
+
+    /**
+     * Converts a file / folder's attributes into a mask
+     *
+     * @param isReg {@code true} if this is a normal file
+     * @param isDir {@code true} if this is a directory
+     * @param isLnk {@code true} if this is a symbolic link
+     * @param perms The file / folder's access {@link PosixFilePermission}s
+     * @return A mask encoding the file / folder's attributes
+     */
+    public static int attributesToPermissions(boolean isReg, boolean isDir, boolean isLnk, Collection<PosixFilePermission> perms) {
+        int pf = 0;
+        if (perms != null) {
+            for (PosixFilePermission p : perms) {
+                switch (p) {
+                    case OWNER_READ:
+                        pf |= SftpConstants.S_IRUSR;
+                        break;
+                    case OWNER_WRITE:
+                        pf |= SftpConstants.S_IWUSR;
+                        break;
+                    case OWNER_EXECUTE:
+                        pf |= SftpConstants.S_IXUSR;
+                        break;
+                    case GROUP_READ:
+                        pf |= SftpConstants.S_IRGRP;
+                        break;
+                    case GROUP_WRITE:
+                        pf |= SftpConstants.S_IWGRP;
+                        break;
+                    case GROUP_EXECUTE:
+                        pf |= SftpConstants.S_IXGRP;
+                        break;
+                    case OTHERS_READ:
+                        pf |= SftpConstants.S_IROTH;
+                        break;
+                    case OTHERS_WRITE:
+                        pf |= SftpConstants.S_IWOTH;
+                        break;
+                    case OTHERS_EXECUTE:
+                        pf |= SftpConstants.S_IXOTH;
+                        break;
+                    default: // ignored
+                }
+            }
+        }
+        pf |= isReg ? SftpConstants.S_IFREG : 0;
+        pf |= isDir ? SftpConstants.S_IFDIR : 0;
+        pf |= isLnk ? SftpConstants.S_IFLNK : 0;
+        return pf;
+    }
+
+    /**
+     * Translates a mask of permissions into its enumeration values equivalents
+     *
+     * @param perms The permissions mask
+     * @return A {@link Set} of the equivalent {@link PosixFilePermission}s
+     */
+    public static Set<PosixFilePermission> permissionsToAttributes(int perms) {
+        Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
+        if ((perms & SftpConstants.S_IRUSR) != 0) {
+            p.add(PosixFilePermission.OWNER_READ);
+        }
+        if ((perms & SftpConstants.S_IWUSR) != 0) {
+            p.add(PosixFilePermission.OWNER_WRITE);
+        }
+        if ((perms & SftpConstants.S_IXUSR) != 0) {
+            p.add(PosixFilePermission.OWNER_EXECUTE);
+        }
+        if ((perms & SftpConstants.S_IRGRP) != 0) {
+            p.add(PosixFilePermission.GROUP_READ);
+        }
+        if ((perms & SftpConstants.S_IWGRP) != 0) {
+            p.add(PosixFilePermission.GROUP_WRITE);
+        }
+        if ((perms & SftpConstants.S_IXGRP) != 0) {
+            p.add(PosixFilePermission.GROUP_EXECUTE);
+        }
+        if ((perms & SftpConstants.S_IROTH) != 0) {
+            p.add(PosixFilePermission.OTHERS_READ);
+        }
+        if ((perms & SftpConstants.S_IWOTH) != 0) {
+            p.add(PosixFilePermission.OTHERS_WRITE);
+        }
+        if ((perms & SftpConstants.S_IXOTH) != 0) {
+            p.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+        return p;
+    }
+
+    /**
+     * Returns the most adequate sub-status for the provided exception
+     *
+     * @param t The thrown {@link Throwable}
+     * @return The matching sub-status
+     */
+    public static int resolveSubstatus(Throwable t) {
+        if ((t instanceof NoSuchFileException) || (t instanceof FileNotFoundException)) {
+            return SftpConstants.SSH_FX_NO_SUCH_FILE;
+        } else if (t instanceof InvalidHandleException) {
+            return SftpConstants.SSH_FX_INVALID_HANDLE;
+        } else if (t instanceof FileAlreadyExistsException) {
+            return SftpConstants.SSH_FX_FILE_ALREADY_EXISTS;
+        } else if (t instanceof DirectoryNotEmptyException) {
+            return SftpConstants.SSH_FX_DIR_NOT_EMPTY;
+        } else if (t instanceof NotDirectoryException) {
+            return SftpConstants.SSH_FX_NOT_A_DIRECTORY;
+        } else if (t instanceof AccessDeniedException) {
+            return SftpConstants.SSH_FX_PERMISSION_DENIED;
+        } else if (t instanceof EOFException) {
+            return SftpConstants.SSH_FX_EOF;
+        } else if (t instanceof OverlappingFileLockException) {
+            return SftpConstants.SSH_FX_LOCK_CONFLICT;
+        } else if (t instanceof UnsupportedOperationException) {
+            return SftpConstants.SSH_FX_OP_UNSUPPORTED;
+        } else if (t instanceof InvalidPathException) {
+            return SftpConstants.SSH_FX_INVALID_FILENAME;
+        } else if (t instanceof IllegalArgumentException) {
+            return SftpConstants.SSH_FX_INVALID_PARAMETER;
+        } else {
+            return SftpConstants.SSH_FX_FAILURE;
+        }
+    }
+
+    public static AclEntry buildAclEntry(int aclType, int aclFlag, int aclMask, final String aclWho) {
+        AclEntryType type;
+        switch (aclType) {
+            case SftpConstants.ACE4_ACCESS_ALLOWED_ACE_TYPE:
+                type = AclEntryType.ALLOW;
+                break;
+            case SftpConstants.ACE4_ACCESS_DENIED_ACE_TYPE:
+                type = AclEntryType.DENY;
+                break;
+            case SftpConstants.ACE4_SYSTEM_AUDIT_ACE_TYPE:
+                type = AclEntryType.AUDIT;
+                break;
+            case SftpConstants.ACE4_SYSTEM_ALARM_ACE_TYPE:
+                type = AclEntryType.AUDIT;
+                break;
+            default:
+                throw new IllegalStateException("Unknown acl type: " + aclType);
+        }
+        Set<AclEntryFlag> flags = EnumSet.noneOf(AclEntryFlag.class);
+        if ((aclFlag & SftpConstants.ACE4_FILE_INHERIT_ACE) != 0) {
+            flags.add(AclEntryFlag.FILE_INHERIT);
+        }
+        if ((aclFlag & SftpConstants.ACE4_DIRECTORY_INHERIT_ACE) != 0) {
+            flags.add(AclEntryFlag.DIRECTORY_INHERIT);
+        }
+        if ((aclFlag & SftpConstants.ACE4_NO_PROPAGATE_INHERIT_ACE) != 0) {
+            flags.add(AclEntryFlag.NO_PROPAGATE_INHERIT);
+        }
+        if ((aclFlag & SftpConstants.ACE4_INHERIT_ONLY_ACE) != 0) {
+            flags.add(AclEntryFlag.INHERIT_ONLY);
+        }
+
+        Set<AclEntryPermission> mask = EnumSet.noneOf(AclEntryPermission.class);
+        if ((aclMask & SftpConstants.ACE4_READ_DATA) != 0) {
+            mask.add(AclEntryPermission.READ_DATA);
+        }
+        if ((aclMask & SftpConstants.ACE4_LIST_DIRECTORY) != 0) {
+            mask.add(AclEntryPermission.LIST_DIRECTORY);
+        }
+        if ((aclMask & SftpConstants.ACE4_WRITE_DATA) != 0) {
+            mask.add(AclEntryPermission.WRITE_DATA);
+        }
+        if ((aclMask & SftpConstants.ACE4_ADD_FILE) != 0) {
+            mask.add(AclEntryPermission.ADD_FILE);
+        }
+        if ((aclMask & SftpConstants.ACE4_APPEND_DATA) != 0) {
+            mask.add(AclEntryPermission.APPEND_DATA);
+        }
+        if ((aclMask & SftpConstants.ACE4_ADD_SUBDIRECTORY) != 0) {
+            mask.add(AclEntryPermission.ADD_SUBDIRECTORY);
+        }
+        if ((aclMask & SftpConstants.ACE4_READ_NAMED_ATTRS) != 0) {
+            mask.add(AclEntryPermission.READ_NAMED_ATTRS);
+        }
+        if ((aclMask & SftpConstants.ACE4_WRITE_NAMED_ATTRS) != 0) {
+            mask.add(AclEntryPermission.WRITE_NAMED_ATTRS);
+        }
+        if ((aclMask & SftpConstants.ACE4_EXECUTE) != 0) {
+            mask.add(AclEntryPermission.EXECUTE);
+        }
+        if ((aclMask & SftpConstants.ACE4_DELETE_CHILD) != 0) {
+            mask.add(AclEntryPermission.DELETE_CHILD);
+        }
+        if ((aclMask & SftpConstants.ACE4_READ_ATTRIBUTES) != 0) {
+            mask.add(AclEntryPermission.READ_ATTRIBUTES);
+        }
+        if ((aclMask & SftpConstants.ACE4_WRITE_ATTRIBUTES) != 0) {
+            mask.add(AclEntryPermission.WRITE_ATTRIBUTES);
+        }
+        if ((aclMask & SftpConstants.ACE4_DELETE) != 0) {
+            mask.add(AclEntryPermission.DELETE);
+        }
+        if ((aclMask & SftpConstants.ACE4_READ_ACL) != 0) {
+            mask.add(AclEntryPermission.READ_ACL);
+        }
+        if ((aclMask & SftpConstants.ACE4_WRITE_ACL) != 0) {
+            mask.add(AclEntryPermission.WRITE_ACL);
+        }
+        if ((aclMask & SftpConstants.ACE4_WRITE_OWNER) != 0) {
+            mask.add(AclEntryPermission.WRITE_OWNER);
+        }
+        if ((aclMask & SftpConstants.ACE4_SYNCHRONIZE) != 0) {
+            mask.add(AclEntryPermission.SYNCHRONIZE);
+        }
+        UserPrincipal who = new DefaultGroupPrincipal(aclWho);
+        return AclEntry.newBuilder()
+                .setType(type)
+                .setFlags(flags)
+                .setPermissions(mask)
+                .setPrincipal(who)
+                .build();
+    }
+
+    public static Map<String, Object> readAttrs(Buffer buffer, int version) {
+        Map<String, Object> attrs = new TreeMap<>();
+        int flags = buffer.getInt();
+        if (version >= SftpConstants.SFTP_V4) {
+            int type = buffer.getUByte();
+            switch (type) {
+                case SftpConstants.SSH_FILEXFER_TYPE_REGULAR:
+                    attrs.put("isRegular", Boolean.TRUE);
+                    break;
+                case SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY:
+                    attrs.put("isDirectory", Boolean.TRUE);
+                    break;
+                case SftpConstants.SSH_FILEXFER_TYPE_SYMLINK:
+                    attrs.put("isSymbolicLink", Boolean.TRUE);
+                    break;
+                case SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN:
+                    attrs.put("isOther", Boolean.TRUE);
+                    break;
+                default:    // ignored
+            }
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+            attrs.put("size", buffer.getLong());
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0) {
+            attrs.put("allocationSize", buffer.getLong());
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
+            attrs.put("uid", buffer.getInt());
+            attrs.put("gid", buffer.getInt());
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+            attrs.put("owner", new DefaultGroupPrincipal(buffer.getString()));
+            attrs.put("group", new DefaultGroupPrincipal(buffer.getString()));
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+            attrs.put("permissions", permissionsToAttributes(buffer.getInt()));
+        }
+
+        if (version == SftpConstants.SFTP_V3) {
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+                attrs.put("lastAccessTime", readTime(buffer, version, flags));
+                attrs.put("lastModifiedTime", readTime(buffer, version, flags));
+            }
+        } else if (version >= SftpConstants.SFTP_V4) {
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+                attrs.put("lastAccessTime", readTime(buffer, version, flags));
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+                attrs.put("creationTime", readTime(buffer, version, flags));
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+                attrs.put("lastModifiedTime", readTime(buffer, version, flags));
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CTIME) != 0) {
+                attrs.put("ctime", readTime(buffer, version, flags));
+            }
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
+            int count = buffer.getInt();
+            List<AclEntry> acls = new ArrayList<>();
+            for (int i = 0; i < count; i++) {
+                int aclType = buffer.getInt();
+                int aclFlag = buffer.getInt();
+                int aclMask = buffer.getInt();
+                String aclWho = buffer.getString();
+                acls.add(buildAclEntry(aclType, aclFlag, aclMask, aclWho));
+            }
+            attrs.put("acl", acls);
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_BITS) != 0) {
+            int bits = buffer.getInt();
+            int valid = 0xffffffff;
+            if (version >= SftpConstants.SFTP_V6) {
+                valid = buffer.getInt();
+            }
+            // TODO: handle attrib bits
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_TEXT_HINT) != 0) {
+            boolean text = buffer.getBoolean();
+            // TODO: handle text
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MIME_TYPE) != 0) {
+            String mimeType = buffer.getString();
+            // TODO: handle mime-type
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_LINK_COUNT) != 0) {
+            int nlink = buffer.getInt();
+            // TODO: handle link-count
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) {
+            String untranslated = buffer.getString();
+            // TODO: handle untranslated-name
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
+            int count = buffer.getInt();
+            Map<String, String> extended = new TreeMap<>();
+            for (int i = 0; i < count; i++) {
+                String key = buffer.getString();
+                String val = buffer.getString();
+                extended.put(key, val);
+            }
+            attrs.put("extended", extended);
+        }
+
+        return attrs;
+    }
+
+    // for v3 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#page-8
+    // for v4 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#page-10
+    // for v6 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-16
+
+    /**
+     * Encodes a {@link FileTime} value into a buffer
+     *
+     * @param buffer The target {@link Buffer}
+     * @param version The encoding version
+     * @param flags The encoding flags
+     * @param time The value to encode
+     */
+    public static void writeTime(Buffer buffer, int version, int flags, FileTime time) {
+        if (version >= SftpConstants.SFTP_V4) {
+            buffer.putLong(time.to(TimeUnit.SECONDS));
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                long nanos = time.to(TimeUnit.NANOSECONDS);
+                nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                buffer.putInt((int) nanos);
+            }
+        } else {
+            buffer.putInt(time.to(TimeUnit.SECONDS));
+        }
+    }
+
+    /**
+     * Decodes a {@link FileTime} value from a buffer
+     *
+     * @param buffer The source {@link Buffer}
+     * @param version The encoding version
+     * @param flags The encoding flags
+     * @return The decoded value
+     */
+    public static FileTime readTime(Buffer buffer, int version, int flags) {
+        long secs = (version >= SftpConstants.SFTP_V4) ? buffer.getLong() : buffer.getUInt();
+        long millis = TimeUnit.SECONDS.toMillis(secs);
+        if ((version >= SftpConstants.SFTP_V4) && ((flags & SftpConstants.SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0)) {
+            long nanoseconds = buffer.getUInt();
+            millis += TimeUnit.NANOSECONDS.toMillis(nanoseconds);
+        }
+        return FileTime.from(millis, TimeUnit.MILLISECONDS);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
index c82dabf..faffd01 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
@@ -21,6 +21,7 @@ package org.apache.sshd.common.util;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -28,6 +29,7 @@ import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -214,10 +216,18 @@ public final class GenericUtils {
         return length(a) <= 0;
     }
 
+    public static boolean isEmpty(int[] a) {
+        return length(a) <= 0;
+    }
+
     public static int length(byte... a) {
         return a == null ? 0 : a.length;
     }
 
+    public static int length(int... a) {
+        return a == null ? 0 : a.length;
+    }
+
     @SafeVarargs
     public static <T> int length(T... a) {
         return a == null ? 0 : a.length;
@@ -268,6 +278,30 @@ public final class GenericUtils {
         return result;
     }
 
+    @SafeVarargs
+    public static <T> List<T> asList(T ... values) {
+        int len = length(values);
+        if (len <= 0) {
+            return Collections.emptyList();
+        } else {
+            return Arrays.asList(values);
+        }
+    }
+
+    public static List<Integer> asList(int ... values) {
+        int len = length(values);
+        if (len <= 0) {
+            return Collections.emptyList();
+        }
+
+        List<Integer> l = new ArrayList<>(len);
+        for (int v : values) {
+            l.add(Integer.valueOf(v));
+        }
+
+        return l;
+    }
+
     @SuppressWarnings({"unchecked", "rawtypes"})
     public static <V extends Comparable<V>> Comparator<V> naturalComparator() {
         // TODO for JDK-8 use Comparator.naturalOrder()

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/beffd2aa/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
index 89c5fd5..b48f811 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
@@ -80,10 +80,28 @@ public final class ValidateUtils {
         return t;
     }
 
-    public static byte[] checkNotNullAndNotEmpty(byte[] t, String message, Object... args) {
-        t = checkNotNull(t, message, args);
-        checkTrue(GenericUtils.length(t) > 0, message, args);
-        return t;
+    public static byte[] checkNotNullAndNotEmpty(byte[] a, String message) {
+        a = checkNotNull(a, message);
+        checkTrue(GenericUtils.length(a) > 0, message);
+        return a;
+    }
+
+    public static byte[] checkNotNullAndNotEmpty(byte[] a, String message, Object... args) {
+        a = checkNotNull(a, message, args);
+        checkTrue(GenericUtils.length(a) > 0, message, args);
+        return a;
+    }
+
+    public static int[] checkNotNullAndNotEmpty(int[] a, String message) {
+        a = checkNotNull(a, message);
+        checkTrue(GenericUtils.length(a) > 0, message);
+        return a;
+    }
+
+    public static int[] checkNotNullAndNotEmpty(int[] a, String message, Object... args) {
+        a = checkNotNull(a, message, args);
+        checkTrue(GenericUtils.length(a) > 0, message, args);
+        return a;
     }
 
     public static <T> T[] checkNotNullAndNotEmpty(T[] t, String message, Object... args) {