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:57 UTC

[10/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/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
new file mode 100644
index 0000000..ad6234c
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
@@ -0,0 +1,330 @@
+/*
+ * 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.util.Collections;
+import java.util.Map;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.logging.LoggingUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@SuppressWarnings("PMD.AvoidUsingOctalValues")
+public final class SftpConstants {
+    public static final String SFTP_SUBSYSTEM_NAME = "sftp";
+
+    public static final int SSH_FXP_INIT = 1;
+    public static final int SSH_FXP_VERSION = 2;
+    public static final int SSH_FXP_OPEN = 3;
+    public static final int SSH_FXP_CLOSE = 4;
+    public static final int SSH_FXP_READ = 5;
+    public static final int SSH_FXP_WRITE = 6;
+    public static final int SSH_FXP_LSTAT = 7;
+    public static final int SSH_FXP_FSTAT = 8;
+    public static final int SSH_FXP_SETSTAT = 9;
+    public static final int SSH_FXP_FSETSTAT = 10;
+    public static final int SSH_FXP_OPENDIR = 11;
+    public static final int SSH_FXP_READDIR = 12;
+    public static final int SSH_FXP_REMOVE = 13;
+    public static final int SSH_FXP_MKDIR = 14;
+    public static final int SSH_FXP_RMDIR = 15;
+    public static final int SSH_FXP_REALPATH = 16;
+    public static final int SSH_FXP_STAT = 17;
+    public static final int SSH_FXP_RENAME = 18;
+    public static final int SSH_FXP_READLINK = 19;
+    public static final int SSH_FXP_SYMLINK = 20; // v3 -> v5
+    public static final int SSH_FXP_LINK = 21; // v6
+    public static final int SSH_FXP_BLOCK = 22; // v6
+    public static final int SSH_FXP_UNBLOCK = 23; // v6
+    public static final int SSH_FXP_STATUS = 101;
+    public static final int SSH_FXP_HANDLE = 102;
+    public static final int SSH_FXP_DATA = 103;
+    public static final int SSH_FXP_NAME = 104;
+    public static final int SSH_FXP_ATTRS = 105;
+    public static final int SSH_FXP_EXTENDED = 200;
+    public static final int SSH_FXP_EXTENDED_REPLY = 201;
+
+    public static final int SSH_FX_OK = 0;
+    public static final int SSH_FX_EOF = 1;
+    public static final int SSH_FX_NO_SUCH_FILE = 2;
+    public static final int SSH_FX_PERMISSION_DENIED = 3;
+    public static final int SSH_FX_FAILURE = 4;
+    public static final int SSH_FX_BAD_MESSAGE = 5;
+    public static final int SSH_FX_NO_CONNECTION = 6;
+    public static final int SSH_FX_CONNECTION_LOST = 7;
+    public static final int SSH_FX_OP_UNSUPPORTED = 8;
+    public static final int SSH_FX_INVALID_HANDLE = 9;
+    public static final int SSH_FX_NO_SUCH_PATH = 10;
+    public static final int SSH_FX_FILE_ALREADY_EXISTS = 11;
+    public static final int SSH_FX_WRITE_PROTECT = 12;
+    public static final int SSH_FX_NO_MEDIA = 13;
+    public static final int SSH_FX_NO_SPACE_ON_FILESYSTEM = 14;
+    public static final int SSH_FX_QUOTA_EXCEEDED = 15;
+    public static final int SSH_FX_UNKNOWN_PRINCIPAL = 16;
+    public static final int SSH_FX_LOCK_CONFLICT = 17;
+    public static final int SSH_FX_DIR_NOT_EMPTY = 18;
+    public static final int SSH_FX_NOT_A_DIRECTORY = 19;
+    public static final int SSH_FX_INVALID_FILENAME = 20;
+    public static final int SSH_FX_LINK_LOOP = 21;
+    public static final int SSH_FX_CANNOT_DELETE = 22;
+    public static final int SSH_FX_INVALID_PARAMETER = 23;
+    public static final int SSH_FX_FILE_IS_A_DIRECTORY = 24;
+    public static final int SSH_FX_BYTE_RANGE_LOCK_CONFLICT = 25;
+    public static final int SSH_FX_BYTE_RANGE_LOCK_REFUSED = 26;
+    public static final int SSH_FX_DELETE_PENDING = 27;
+    public static final int SSH_FX_FILE_CORRUPT = 28;
+    public static final int SSH_FX_OWNER_INVALID = 29;
+    public static final int SSH_FX_GROUP_INVALID = 30;
+    public static final int SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK = 31;
+
+    public static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001;
+    public static final int SSH_FILEXFER_ATTR_UIDGID = 0x00000002;
+    public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004;
+    public static final int SSH_FILEXFER_ATTR_ACMODTIME = 0x00000008; // v3 naming convention
+    public static final int SSH_FILEXFER_ATTR_ACCESSTIME = 0x00000008; // v4
+    public static final int SSH_FILEXFER_ATTR_CREATETIME = 0x00000010; // v4
+    public static final int SSH_FILEXFER_ATTR_MODIFYTIME = 0x00000020; // v4
+    public static final int SSH_FILEXFER_ATTR_ACL = 0x00000040; // v4
+    public static final int SSH_FILEXFER_ATTR_OWNERGROUP = 0x00000080; // v4
+    public static final int SSH_FILEXFER_ATTR_SUBSECOND_TIMES = 0x00000100; // v5
+    public static final int SSH_FILEXFER_ATTR_BITS = 0x00000200; // v5
+    public static final int SSH_FILEXFER_ATTR_ALLOCATION_SIZE = 0x00000400; // v6
+    public static final int SSH_FILEXFER_ATTR_TEXT_HINT = 0x00000800; // v6
+    public static final int SSH_FILEXFER_ATTR_MIME_TYPE = 0x00001000; // v6
+    public static final int SSH_FILEXFER_ATTR_LINK_COUNT = 0x00002000; // v6
+    public static final int SSH_FILEXFER_ATTR_UNTRANSLATED_NAME = 0x00004000; // v6
+    public static final int SSH_FILEXFER_ATTR_CTIME = 0x00008000; // v6
+    public static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000;
+
+    public static final int SSH_FILEXFER_ATTR_ALL = 0x0000FFFF; // All attributes
+
+    public static final int SSH_FILEXFER_ATTR_FLAGS_READONLY = 0x00000001;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_SYSTEM = 0x00000002;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_HIDDEN = 0x00000004;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE = 0x00000008;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_ARCHIVE = 0x00000010;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED = 0x00000020;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_COMPRESSED = 0x00000040;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_SPARSE = 0x00000080;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY = 0x00000100;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE = 0x00000200;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_SYNC = 0x00000400;
+
+    public static final int SSH_FILEXFER_TYPE_REGULAR = 1;
+    public static final int SSH_FILEXFER_TYPE_DIRECTORY = 2;
+    public static final int SSH_FILEXFER_TYPE_SYMLINK = 3;
+    public static final int SSH_FILEXFER_TYPE_SPECIAL = 4;
+    public static final int SSH_FILEXFER_TYPE_UNKNOWN = 5;
+    public static final int SSH_FILEXFER_TYPE_SOCKET = 6; // v5
+    public static final int SSH_FILEXFER_TYPE_CHAR_DEVICE = 7; // v5
+    public static final int SSH_FILEXFER_TYPE_BLOCK_DEVICE = 8; // v5
+    public static final int SSH_FILEXFER_TYPE_FIFO = 9; // v5
+
+    public static final int SSH_FXF_READ = 0x00000001;
+    public static final int SSH_FXF_WRITE = 0x00000002;
+    public static final int SSH_FXF_APPEND = 0x00000004;
+    public static final int SSH_FXF_CREAT = 0x00000008;
+    public static final int SSH_FXF_TRUNC = 0x00000010;
+    public static final int SSH_FXF_EXCL = 0x00000020;
+    public static final int SSH_FXF_TEXT = 0x00000040;
+
+    public static final int SSH_FXF_ACCESS_DISPOSITION = 0x00000007;
+    public static final int SSH_FXF_CREATE_NEW = 0x00000000;
+    public static final int SSH_FXF_CREATE_TRUNCATE = 0x00000001;
+    public static final int SSH_FXF_OPEN_EXISTING = 0x00000002;
+    public static final int SSH_FXF_OPEN_OR_CREATE = 0x00000003;
+    public static final int SSH_FXF_TRUNCATE_EXISTING = 0x00000004;
+    public static final int SSH_FXF_APPEND_DATA = 0x00000008;
+    public static final int SSH_FXF_APPEND_DATA_ATOMIC = 0x00000010;
+    public static final int SSH_FXF_TEXT_MODE = 0x00000020;
+    public static final int SSH_FXF_READ_LOCK = 0x00000040;
+    public static final int SSH_FXF_WRITE_LOCK = 0x00000080;
+    public static final int SSH_FXF_DELETE_LOCK = 0x00000100;
+    public static final int SSH_FXF_BLOCK_ADVISORY = 0x00000200;
+    public static final int SSH_FXF_NOFOLLOW = 0x00000400;
+    public static final int SSH_FXF_DELETE_ON_CLOSE = 0x00000800;
+    public static final int SSH_FXF_ACCESS_AUDIT_ALARM_INFO = 0x00001000;
+    public static final int SSH_FXF_ACCESS_BACKUP = 0x00002000;
+    public static final int SSH_FXF_BACKUP_STREAM = 0x00004000;
+    public static final int SSH_FXF_OVERRIDE_OWNER = 0x00008000;
+
+    public static final int SSH_FXP_RENAME_OVERWRITE = 0x00000001;
+    public static final int SSH_FXP_RENAME_ATOMIC = 0x00000002;
+    public static final int SSH_FXP_RENAME_NATIVE = 0x00000004;
+
+    public static final int SSH_FXP_REALPATH_NO_CHECK = 0x00000001;
+    public static final int SSH_FXP_REALPATH_STAT_IF = 0x00000002;
+    public static final int SSH_FXP_REALPATH_STAT_ALWAYS = 0x00000003;
+
+    public static final int SSH_FXF_RENAME_OVERWRITE = 0x00000001;
+    public static final int SSH_FXF_RENAME_ATOMIC = 0x00000002;
+    public static final int SSH_FXF_RENAME_NATIVE = 0x00000004;
+
+    public static final int SFX_ACL_CONTROL_INCLUDED = 0x00000001;
+    public static final int SFX_ACL_CONTROL_PRESENT = 0x00000002;
+    public static final int SFX_ACL_CONTROL_INHERITED = 0x00000004;
+    public static final int SFX_ACL_AUDIT_ALARM_INCLUDED = 0x00000010;
+    public static final int SFX_ACL_AUDIT_ALARM_INHERITED = 0x00000020;
+
+    public static final int ACE4_ACCESS_ALLOWED_ACE_TYPE = 0x00000000;
+    public static final int ACE4_ACCESS_DENIED_ACE_TYPE = 0x00000001;
+    public static final int ACE4_SYSTEM_AUDIT_ACE_TYPE = 0x00000002;
+    public static final int ACE4_SYSTEM_ALARM_ACE_TYPE = 0x00000003;
+
+    public static final int ACE4_FILE_INHERIT_ACE = 0x00000001;
+    public static final int ACE4_DIRECTORY_INHERIT_ACE = 0x00000002;
+    public static final int ACE4_NO_PROPAGATE_INHERIT_ACE = 0x00000004;
+    public static final int ACE4_INHERIT_ONLY_ACE = 0x00000008;
+    public static final int ACE4_SUCCESSFUL_ACCESS_ACE_FLAG = 0x00000010;
+    public static final int ACE4_FAILED_ACCESS_ACE_FLAG = 0x00000020;
+    public static final int ACE4_IDENTIFIER_GROUP = 0x00000040;
+
+    public static final int ACE4_READ_DATA = 0x00000001;
+    public static final int ACE4_LIST_DIRECTORY = 0x00000001;
+    public static final int ACE4_WRITE_DATA = 0x00000002;
+    public static final int ACE4_ADD_FILE = 0x00000002;
+    public static final int ACE4_APPEND_DATA = 0x00000004;
+    public static final int ACE4_ADD_SUBDIRECTORY = 0x00000004;
+    public static final int ACE4_READ_NAMED_ATTRS = 0x00000008;
+    public static final int ACE4_WRITE_NAMED_ATTRS = 0x00000010;
+    public static final int ACE4_EXECUTE = 0x00000020;
+    public static final int ACE4_DELETE_CHILD = 0x00000040;
+    public static final int ACE4_READ_ATTRIBUTES = 0x00000080;
+    public static final int ACE4_WRITE_ATTRIBUTES = 0x00000100;
+    public static final int ACE4_DELETE = 0x00010000;
+    public static final int ACE4_READ_ACL = 0x00020000;
+    public static final int ACE4_WRITE_ACL = 0x00040000;
+    public static final int ACE4_WRITE_OWNER = 0x00080000;
+    public static final int ACE4_SYNCHRONIZE = 0x00100000;
+
+    public static final int S_IFMT = 0170000;  // bitmask for the file type bitfields
+    public static final int S_IFSOCK = 0140000;  // socket
+    public static final int S_IFLNK = 0120000;  // symbolic link
+    public static final int S_IFREG = 0100000;  // regular file
+    public static final int S_IFBLK = 0060000;  // block device
+    public static final int S_IFDIR = 0040000;  // directory
+    public static final int S_IFCHR = 0020000;  // character device
+    public static final int S_IFIFO = 0010000;  // fifo
+    public static final int S_ISUID = 0004000;  // set UID bit
+    public static final int S_ISGID = 0002000;  // set GID bit
+    public static final int S_ISVTX = 0001000;  // sticky bit
+    public static final int S_IRUSR = 0000400;
+    public static final int S_IWUSR = 0000200;
+    public static final int S_IXUSR = 0000100;
+    public static final int S_IRGRP = 0000040;
+    public static final int S_IWGRP = 0000020;
+    public static final int S_IXGRP = 0000010;
+    public static final int S_IROTH = 0000004;
+    public static final int S_IWOTH = 0000002;
+    public static final int S_IXOTH = 0000001;
+
+    public static final int SFTP_V3 = 3;
+    public static final int SFTP_V4 = 4;
+    public static final int SFTP_V5 = 5;
+    public static final int SFTP_V6 = 6;
+
+    // (Some) names of known extensions
+    public static final String EXT_VERSIONS = "versions";
+    public static final String EXT_NEWLINE = "newline";
+    public static final String EXT_VENDOR_ID = "vendor-id";
+    public static final String EXT_SUPPORTED = "supported";
+    public static final String EXT_SUPPORTED2 = "supported2";
+    public static final String EXT_TEXT_SEEK = "text-seek";
+    public static final String EXT_VERSION_SELECT = "version-select";
+    public static final String EXT_COPY_FILE = "copy-file";
+
+    public static final String EXT_MD5_HASH = "md5-hash";
+    public static final String EXT_MD5_HASH_HANDLE = "md5-hash-handle";
+    public static final int MD5_QUICK_HASH_SIZE = 2048;
+
+    public static final String EXT_CHECK_FILE_HANDLE = "check-file-handle";
+    public static final String EXT_CHECK_FILE_NAME = "check-file-name";
+    public static final int MIN_CHKFILE_BLOCKSIZE = 256;
+
+    public static final String EXT_CHECK_FILE = "check-file";
+    public static final String EXT_COPY_DATA = "copy-data";
+    public static final String EXT_SPACE_AVAILABLE = "space-available";
+
+    // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-11 section 5.4
+    public static final String EXT_ACL_SUPPORTED = "acl-supported";
+    public static final int SSH_ACL_CAP_ALLOW = 0x00000001;
+    public static final int SSH_ACL_CAP_DENY = 0x00000002;
+    public static final int SSH_ACL_CAP_AUDIT = 0x00000004;
+    public static final int SSH_ACL_CAP_ALARM = 0x00000008;
+    public static final int SSH_ACL_CAP_INHERIT_ACCESS = 0x00000010;
+    public static final int SSH_ACL_CAP_INHERIT_AUDIT_ALARM = 0x00000020;
+
+    private SftpConstants() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    private static class LazyCommandNameHolder {
+        private static final Map<Integer, String> NAMES_MAP =
+                Collections.unmodifiableMap(
+                    LoggingUtils.generateMnemonicMap(SftpConstants.class, f -> {
+                        String name = f.getName();
+                        return name.startsWith("SSH_FXP_")
+                            // exclude the rename modes which are not opcodes
+                            && (!name.startsWith("SSH_FXP_RENAME_"))
+                            // exclude the realpath modes wich are not opcodes
+                            && (!name.startsWith("SSH_FXP_REALPATH_"));
+                    }));
+    }
+
+    /**
+     * Converts a command value to a user-friendly name
+     *
+     * @param cmd The command value
+     * @return The user-friendly name - if not one of the defined {@code SSH_FXP_XXX}
+     * values then returns the string representation of the command's value
+     */
+    public static String getCommandMessageName(int cmd) {
+        @SuppressWarnings("synthetic-access")
+        String name = LazyCommandNameHolder.NAMES_MAP.get(cmd);
+        if (GenericUtils.isEmpty(name)) {
+            return Integer.toString(cmd);
+        } else {
+            return name;
+        }
+    }
+
+    private static class LazyStatusNameHolder {
+        private static final Map<Integer, String> STATUS_MAP =
+                Collections.unmodifiableMap(LoggingUtils.generateMnemonicMap(SftpConstants.class, "SSH_FX_"));
+    }
+
+    /**
+     * Converts a return status value to a user-friendly name
+     *
+     * @param status The status value
+     * @return The user-friendly name - if not one of the defined {@code SSH_FX_XXX}
+     * values then returns the string representation of the status value
+     */
+    public static String getStatusName(int status) {
+        @SuppressWarnings("synthetic-access")
+        String name = LazyStatusNameHolder.STATUS_MAP.get(status);
+        if (GenericUtils.isEmpty(name)) {
+            return Integer.toString(status);
+        } else {
+            return name;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpException.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpException.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpException.java
new file mode 100644
index 0000000..b7fd157
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpException.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.common.subsystem.sftp;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="http://mina.apache.org">Apache MINA Project</a>
+ */
+public class SftpException extends IOException {
+    private static final long serialVersionUID = 8096963562429466995L;
+    private final int status;
+
+    public SftpException(int status, String msg) {
+        super(msg);
+        this.status = status;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    @Override
+    public String toString() {
+        return "SFTP error (" + SftpConstants.getStatusName(getStatus()) + "): " + getMessage();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
new file mode 100644
index 0000000..9404e1e
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
@@ -0,0 +1,1114 @@
+/*
+ * 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.net.UnknownServiceException;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileSystemLoopException;
+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.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalNotFoundException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.server.subsystem.sftp.DefaultGroupPrincipal;
+import org.apache.sshd.server.subsystem.sftp.InvalidHandleException;
+import org.apache.sshd.server.subsystem.sftp.UnixDateFormat;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SftpHelper {
+    /**
+     * Used to control whether to append the end-of-list indicator for
+     * SSH_FXP_NAME responses via {@link #indicateEndOfNamesList(Buffer, int, PropertyResolver, Boolean)}
+     * call, as indicated by <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     */
+    public static final String APPEND_END_OF_LIST_INDICATOR = "sftp-append-eol-indicator";
+
+    /**
+     * Default value for {@link #APPEND_END_OF_LIST_INDICATOR} if none configured
+     */
+    public static final boolean DEFAULT_APPEND_END_OF_LIST_INDICATOR = true;
+
+    public static final NavigableMap<Integer, String> DEFAULT_SUBSTATUS_MESSAGE =
+        Collections.unmodifiableNavigableMap(new TreeMap<Integer, String>(Comparator.naturalOrder()) {
+            // Not serializing it
+            private static final long serialVersionUID = 1L;
+
+            {
+                put(SftpConstants.SSH_FX_OK, "Success");
+                put(SftpConstants.SSH_FX_EOF, "End of file");
+                put(SftpConstants.SSH_FX_NO_SUCH_FILE, "No such file or directory");
+                put(SftpConstants.SSH_FX_PERMISSION_DENIED, "Permission denied");
+                put(SftpConstants.SSH_FX_FAILURE, "General failure");
+                put(SftpConstants.SSH_FX_BAD_MESSAGE, "Bad message data");
+                put(SftpConstants.SSH_FX_NO_CONNECTION, "No connection to server");
+                put(SftpConstants.SSH_FX_CONNECTION_LOST, "Connection lost");
+                put(SftpConstants.SSH_FX_OP_UNSUPPORTED, "Unsupported operation requested");
+                put(SftpConstants.SSH_FX_INVALID_HANDLE, "Invalid handle value");
+                put(SftpConstants.SSH_FX_NO_SUCH_PATH, "No such path");
+                put(SftpConstants.SSH_FX_FILE_ALREADY_EXISTS, "File/Directory already exists");
+                put(SftpConstants.SSH_FX_WRITE_PROTECT, "File/Directory is write-protected");
+                put(SftpConstants.SSH_FX_NO_MEDIA, "No such meadia");
+                put(SftpConstants.SSH_FX_NO_SPACE_ON_FILESYSTEM, "No space left on device");
+                put(SftpConstants.SSH_FX_QUOTA_EXCEEDED, "Quota exceeded");
+                put(SftpConstants.SSH_FX_UNKNOWN_PRINCIPAL, "Unknown user/group");
+                put(SftpConstants.SSH_FX_LOCK_CONFLICT, "Lock conflict");
+                put(SftpConstants.SSH_FX_DIR_NOT_EMPTY, "Directory not empty");
+                put(SftpConstants.SSH_FX_NOT_A_DIRECTORY, "Accessed location is not a directory");
+                put(SftpConstants.SSH_FX_INVALID_FILENAME, "Invalid filename");
+                put(SftpConstants.SSH_FX_LINK_LOOP, "Link loop");
+                put(SftpConstants.SSH_FX_CANNOT_DELETE, "Cannot remove");
+                put(SftpConstants.SSH_FX_INVALID_PARAMETER, "Invalid parameter");
+                put(SftpConstants.SSH_FX_FILE_IS_A_DIRECTORY, "Accessed location is a directory");
+                put(SftpConstants.SSH_FX_BYTE_RANGE_LOCK_CONFLICT, "Range lock conflict");
+                put(SftpConstants.SSH_FX_BYTE_RANGE_LOCK_REFUSED, "Range lock refused");
+                put(SftpConstants.SSH_FX_DELETE_PENDING, "Delete pending");
+                put(SftpConstants.SSH_FX_FILE_CORRUPT, "Corrupted file/directory");
+                put(SftpConstants.SSH_FX_OWNER_INVALID, "Invalid file/directory owner");
+                put(SftpConstants.SSH_FX_GROUP_INVALID, "Invalid file/directory group");
+                put(SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK, "No matching byte range lock");
+            }
+        });
+
+    private SftpHelper() {
+        throw new UnsupportedOperationException("No instance allowed");
+    }
+
+    /**
+     * Retrieves the end-of-file indicator for {@code SSH_FXP_DATA} responses, provided
+     * the version is at least 6, and the buffer has enough available data
+     *
+     * @param buffer  The {@link Buffer} to retrieve the data from
+     * @param version The SFTP version being used
+     * @return The indicator value - {@code null} if none retrieved
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A>
+     */
+    public static Boolean getEndOfFileIndicatorValue(Buffer buffer, int version) {
+        return (version <  SftpConstants.SFTP_V6) || (buffer.available() < 1) ? null : buffer.getBoolean();
+    }
+
+    /**
+     * Retrieves the end-of-list indicator for {@code SSH_FXP_NAME} responses, provided
+     * the version is at least 6, and the buffer has enough available data
+     *
+     * @param buffer  The {@link Buffer} to retrieve the data from
+     * @param version The SFTP version being used
+     * @return The indicator value - {@code null} if none retrieved
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     * @see #indicateEndOfNamesList(Buffer, int, PropertyResolver, Boolean)
+     */
+    public static Boolean getEndOfListIndicatorValue(Buffer buffer, int version) {
+        return (version <  SftpConstants.SFTP_V6) || (buffer.available() < 1) ? null : buffer.getBoolean();
+    }
+
+    /**
+     * Appends the end-of-list={@code TRUE} indicator for {@code SSH_FXP_NAME} responses, provided
+     * the version is at least 6 and the feature is enabled
+     *
+     * @param buffer   The {@link Buffer} to append the indicator
+     * @param version  The SFTP version being used
+     * @param resolver The {@link PropertyResolver} to query whether to enable the feature
+     * @return The actual indicator value used - {@code null} if none appended
+     * @see #indicateEndOfNamesList(Buffer, int, PropertyResolver, boolean)
+     */
+    public static Boolean indicateEndOfNamesList(Buffer buffer, int version, PropertyResolver resolver) {
+        return indicateEndOfNamesList(buffer, version, resolver, true);
+    }
+
+    /**
+     * Appends the end-of-list indicator for {@code SSH_FXP_NAME} responses, provided the version
+     * is at least 6, the feature is enabled and the indicator value is not {@code null}
+     *
+     * @param buffer         The {@link Buffer} to append the indicator
+     * @param version        The SFTP version being used
+     * @param resolver       The {@link PropertyResolver} to query whether to enable the feature
+     * @param indicatorValue The indicator value - {@code null} means don't append the indicator
+     * @return The actual indicator value used - {@code null} if none appended
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     * @see #APPEND_END_OF_LIST_INDICATOR
+     * @see #DEFAULT_APPEND_END_OF_LIST_INDICATOR
+     */
+    public static Boolean indicateEndOfNamesList(Buffer buffer, int version, PropertyResolver resolver, boolean indicatorValue) {
+        if (version < SftpConstants.SFTP_V6) {
+            return null;
+        }
+
+        if (!resolver.getBooleanProperty(APPEND_END_OF_LIST_INDICATOR, DEFAULT_APPEND_END_OF_LIST_INDICATOR)) {
+            return null;
+        }
+
+        buffer.putBoolean(indicatorValue);
+        return indicatorValue;
+    }
+
+    /**
+     * Writes a file / folder's attributes to a buffer
+     *
+     * @param <B> Type of {@link Buffer} being updated
+     * @param buffer The target buffer instance
+     * @param version The output encoding version
+     * @param attributes The {@link Map} of attributes
+     * @return The updated buffer
+     * @see #writeAttrsV3(Buffer, int, Map)
+     * @see #writeAttrsV4(Buffer, int, Map)
+     */
+    public static <B extends Buffer> B writeAttrs(B buffer, int version, Map<String, ?> attributes) {
+        if (version == SftpConstants.SFTP_V3) {
+            return writeAttrsV3(buffer, version, attributes);
+        } else if (version >= SftpConstants.SFTP_V4) {
+            return writeAttrsV4(buffer, version, attributes);
+        } else {
+            throw new IllegalStateException("Unsupported SFTP version: " + version);
+        }
+    }
+
+    /**
+     * Writes the retrieved file / directory attributes in V3 format
+     *
+     * @param <B> Type of {@link Buffer} being updated
+     * @param buffer The target buffer instance
+     * @param version The actual version - must be {@link SftpConstants#SFTP_V3}
+     * @param attributes The {@link Map} of attributes
+     * @return The updated buffer
+     */
+    public static <B extends Buffer> B writeAttrsV3(B 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");
+        Map<?, ?> extensions = (Map<?, ?>) attributes.get("extended");
+        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)
+                  | ((extensions != null) ? SftpConstants.SSH_FILEXFER_ATTR_EXTENDED : 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) {
+            buffer = writeTime(buffer, version, flags, lastAccessTime);
+            buffer = writeTime(buffer, version, flags, lastModifiedTime);
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
+            buffer = writeExtensions(buffer, extensions);
+        }
+
+        return buffer;
+    }
+
+    /**
+     * Writes the retrieved file / directory attributes in V4+ format
+     *
+     * @param <B> Type of {@link Buffer} being updated
+     * @param buffer The target buffer instance
+     * @param version The actual version - must be at least {@link SftpConstants#SFTP_V4}
+     * @param attributes The {@link Map} of attributes
+     * @return The updated buffer
+     */
+    public static <B extends Buffer> B writeAttrsV4(B 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");
+        @SuppressWarnings("unchecked")
+        Collection<AclEntry> acl = (Collection<AclEntry>) attributes.get("acl");
+        Map<?, ?> extensions = (Map<?, ?>) attributes.get("extended");
+        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)
+                  | ((acl != null) ? SftpConstants.SSH_FILEXFER_ATTR_ACL : 0)
+                  | ((extensions != null) ? SftpConstants.SSH_FILEXFER_ATTR_EXTENDED : 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"), SftpUniversalOwnerAndGroup.Owner.getName()));
+            buffer.putString(Objects.toString(attributes.get("group"), SftpUniversalOwnerAndGroup.Group.getName()));
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+            buffer.putInt(attributesToPermissions(isReg, isDir, isLnk, perms));
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+            buffer = writeTime(buffer, version, flags, lastAccessTime);
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+            buffer = writeTime(buffer, version, flags, lastAccessTime);
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+            buffer = writeTime(buffer, version, flags, lastModifiedTime);
+        }
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
+            buffer = writeACLs(buffer, version, acl);
+        }
+        // TODO: ctime
+        // TODO: bits
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
+            buffer = writeExtensions(buffer, extensions);
+        }
+
+        return buffer;
+    }
+
+    /**
+     * @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;
+    }
+
+    /**
+     * Converts a POSIX permissions mask to a file type value
+     *
+     * @param perms The POSIX permissions mask
+     * @return The file type - see {@code SSH_FILEXFER_TYPE_xxx} values
+     */
+    public static int permissionsToFileType(int perms) {
+        if ((SftpConstants.S_IFLNK & perms) == SftpConstants.S_IFLNK) {
+            return SftpConstants.SSH_FILEXFER_TYPE_SYMLINK;
+        } else if ((SftpConstants.S_IFREG & perms) == SftpConstants.S_IFREG) {
+            return SftpConstants.SSH_FILEXFER_TYPE_REGULAR;
+        } else if ((SftpConstants.S_IFDIR & perms) == SftpConstants.S_IFDIR) {
+            return SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY;
+        } else if ((SftpConstants.S_IFSOCK & perms) == SftpConstants.S_IFSOCK) {
+            return SftpConstants.SSH_FILEXFER_TYPE_SOCKET;
+        } else if ((SftpConstants.S_IFBLK & perms) == SftpConstants.S_IFBLK) {
+            return SftpConstants.SSH_FILEXFER_TYPE_BLOCK_DEVICE;
+        } else if ((SftpConstants.S_IFCHR & perms) == SftpConstants.S_IFCHR) {
+            return SftpConstants.SSH_FILEXFER_TYPE_CHAR_DEVICE;
+        } else if ((SftpConstants.S_IFIFO & perms) == SftpConstants.S_IFIFO) {
+            return SftpConstants.SSH_FILEXFER_TYPE_FIFO;
+        } else {
+            return SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN;
+        }
+    }
+
+    /**
+     * Converts a file type into a POSIX permission mask value
+
+     * @param type File type - see {@code SSH_FILEXFER_TYPE_xxx} values
+     * @return The matching POSIX permission mask value
+     */
+    public static int fileTypeToPermission(int type) {
+        switch (type) {
+            case SftpConstants.SSH_FILEXFER_TYPE_REGULAR:
+                return SftpConstants.S_IFREG;
+            case SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY:
+                return SftpConstants.S_IFDIR;
+            case SftpConstants.SSH_FILEXFER_TYPE_SYMLINK:
+                return SftpConstants.S_IFLNK;
+            case SftpConstants.SSH_FILEXFER_TYPE_SOCKET:
+                return SftpConstants.S_IFSOCK;
+            case SftpConstants.SSH_FILEXFER_TYPE_BLOCK_DEVICE:
+                return SftpConstants.S_IFBLK;
+            case SftpConstants.SSH_FILEXFER_TYPE_CHAR_DEVICE:
+                return SftpConstants.S_IFCHR;
+            case SftpConstants.SSH_FILEXFER_TYPE_FIFO:
+                return SftpConstants.S_IFIFO;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * 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
+     */
+    @SuppressWarnings("checkstyle:ReturnCount")
+    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)
+                || (t instanceof UnknownServiceException)) {
+            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 if (t instanceof UserPrincipalNotFoundException) {
+            return SftpConstants.SSH_FX_UNKNOWN_PRINCIPAL;
+        } else if (t instanceof FileSystemLoopException) {
+            return SftpConstants.SSH_FX_LINK_LOOP;
+        } else if (t instanceof SftpException) {
+            return ((SftpException) t).getStatus();
+        } else {
+            return SftpConstants.SSH_FX_FAILURE;
+        }
+    }
+
+    public static String resolveStatusMessage(int subStatus) {
+        String message = DEFAULT_SUBSTATUS_MESSAGE.get(subStatus);
+        return GenericUtils.isEmpty(message) ? ("Unknown error: " + subStatus) : message;
+    }
+
+    public static NavigableMap<String, Object> readAttrs(Buffer buffer, int version) {
+        NavigableMap<String, Object> attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        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_SOCKET:
+                case SftpConstants.SSH_FILEXFER_TYPE_CHAR_DEVICE:
+                case SftpConstants.SSH_FILEXFER_TYPE_BLOCK_DEVICE:
+                case SftpConstants.SSH_FILEXFER_TYPE_FIFO:
+                    attrs.put("isOther", Boolean.TRUE);
+                    break;
+                default:    // ignored
+            }
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+            attrs.put("size", buffer.getLong());
+        }
+
+        if (version == SftpConstants.SFTP_V3) {
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
+                attrs.put("uid", buffer.getInt());
+                attrs.put("gid", buffer.getInt());
+            }
+        } else {
+            if ((version >= SftpConstants.SFTP_V6) && ((flags & SftpConstants.SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0)) {
+                @SuppressWarnings("unused")
+                long allocSize = buffer.getLong();    // TODO handle allocation size
+            }
+
+            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 ((version >= SftpConstants.SFTP_V6) && (flags & SftpConstants.SSH_FILEXFER_ATTR_CTIME) != 0) {
+                attrs.put("ctime", readTime(buffer, version, flags));
+            }
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
+                attrs.put("acl", readACLs(buffer, version));
+            }
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_BITS) != 0) {
+                @SuppressWarnings("unused")
+                int bits = buffer.getInt();
+                @SuppressWarnings("unused")
+                int valid = 0xffffffff;
+                if (version >= SftpConstants.SFTP_V6) {
+                    valid = buffer.getInt();
+                }
+                // TODO: handle attrib bits
+            }
+
+            if (version >= SftpConstants.SFTP_V6) {
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_TEXT_HINT) != 0) {
+                    @SuppressWarnings("unused")
+                    boolean text = buffer.getBoolean(); // TODO: handle text
+                }
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MIME_TYPE) != 0) {
+                    @SuppressWarnings("unused")
+                    String mimeType = buffer.getString(); // TODO: handle mime-type
+                }
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_LINK_COUNT) != 0) {
+                    @SuppressWarnings("unused")
+                    int nlink = buffer.getInt(); // TODO: handle link-count
+                }
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) {
+                    @SuppressWarnings("unused")
+                    String untranslated = buffer.getString(); // TODO: handle untranslated-name
+                }
+            }
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
+            attrs.put("extended", readExtensions(buffer));
+        }
+
+        return attrs;
+    }
+
+    public static NavigableMap<String, byte[]> readExtensions(Buffer buffer) {
+        int count = buffer.getInt();
+        // NOTE
+        NavigableMap<String, byte[]> extended = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (int i = 0; i < count; i++) {
+            String key = buffer.getString();
+            byte[] val = buffer.getBytes();
+            byte[] prev = extended.put(key, val);
+            ValidateUtils.checkTrue(prev == null, "Duplicate values for extended key=%s", key);
+        }
+
+        return extended;
+    }
+
+    public static  <B extends Buffer> B writeExtensions(B buffer, Map<?, ?> extensions) {
+        int numExtensions = GenericUtils.size(extensions);
+        buffer.putInt(numExtensions);
+        if (numExtensions <= 0) {
+            return buffer;
+        }
+
+        extensions.forEach((key, value) -> {
+            Objects.requireNonNull(key, "No extension type");
+            Objects.requireNonNull(value, "No extension value");
+            buffer.putString(key.toString());
+            if (value instanceof byte[]) {
+                buffer.putBytes((byte[]) value);
+            } else {
+                buffer.putString(value.toString());
+            }
+        });
+
+        return buffer;
+    }
+
+    public static NavigableMap<String, String> toStringExtensions(Map<String, ?> extensions) {
+        if (GenericUtils.isEmpty(extensions)) {
+            return Collections.emptyNavigableMap();
+        }
+
+        // NOTE: even though extensions are probably case sensitive we do not allow duplicate name that differs only in case
+        NavigableMap<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        extensions.forEach((key, value) -> {
+            ValidateUtils.checkNotNull(value, "No value for extension=%s", key);
+            String prev = map.put(key, (value instanceof byte[]) ? new String((byte[]) value, StandardCharsets.UTF_8) : value.toString());
+            ValidateUtils.checkTrue(prev == null, "Multiple values for extension=%s", key);
+        });
+
+        return map;
+    }
+
+    public static NavigableMap<String, byte[]> toBinaryExtensions(Map<String, String> extensions) {
+        if (GenericUtils.isEmpty(extensions)) {
+            return Collections.emptyNavigableMap();
+        }
+
+        // NOTE: even though extensions are probably case sensitive we do not allow duplicate name that differs only in case
+        NavigableMap<String, byte[]> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        extensions.forEach((key, value) -> {
+            ValidateUtils.checkNotNull(value, "No value for extension=%s", key);
+            byte[] prev = map.put(key, value.getBytes(StandardCharsets.UTF_8));
+            ValidateUtils.checkTrue(prev == null, "Multiple values for extension=%s", key);
+        });
+
+        return map;
+    }
+
+    // for v4,5 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#page-15
+    // for v6 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-21
+    public static List<AclEntry> readACLs(Buffer buffer, int version) {
+        int aclSize = buffer.getInt();
+        int startPos = buffer.rpos();
+        Buffer aclBuffer = new ByteArrayBuffer(buffer.array(), startPos, aclSize, true);
+        List<AclEntry> acl = decodeACLs(aclBuffer, version);
+        buffer.rpos(startPos + aclSize);
+        return acl;
+    }
+
+    public static List<AclEntry> decodeACLs(Buffer buffer, int version) {
+        @SuppressWarnings("unused")
+        int aclFlags = 0;   // TODO handle ACL flags
+        if (version >= SftpConstants.SFTP_V6) {
+            aclFlags = buffer.getInt();
+        }
+
+        int count = buffer.getInt();
+        // NOTE: although the value is defined as UINT32 we do not expected a count greater than Integer.MAX_VALUE
+        ValidateUtils.checkTrue(count >= 0, "Invalid ACL entries count: %d", count);
+        if (count == 0) {
+            return Collections.emptyList();
+        }
+
+        List<AclEntry> acls = new ArrayList<>(count);
+        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));
+        }
+
+        return acls;
+    }
+
+    public static AclEntry buildAclEntry(int aclType, int aclFlag, int aclMask, String aclWho) {
+        UserPrincipal who = new DefaultGroupPrincipal(aclWho);
+        return AclEntry.newBuilder()
+                .setType(ValidateUtils.checkNotNull(decodeAclEntryType(aclType), "Unknown ACL type: %d", aclType))
+                .setFlags(decodeAclFlags(aclFlag))
+                .setPermissions(decodeAclMask(aclMask))
+                .setPrincipal(who)
+                .build();
+    }
+
+    /**
+     * @param aclType The {@code ACE4_ACCESS_xxx_ACE_TYPE} value
+     * @return The matching {@link AclEntryType} or {@code null} if unknown value
+     */
+    public static AclEntryType decodeAclEntryType(int aclType) {
+        switch (aclType) {
+            case SftpConstants.ACE4_ACCESS_ALLOWED_ACE_TYPE:
+                return AclEntryType.ALLOW;
+            case SftpConstants.ACE4_ACCESS_DENIED_ACE_TYPE:
+                return AclEntryType.DENY;
+            case SftpConstants.ACE4_SYSTEM_AUDIT_ACE_TYPE:
+                return AclEntryType.AUDIT;
+            case SftpConstants.ACE4_SYSTEM_ALARM_ACE_TYPE:
+                return AclEntryType.ALARM;
+            default:
+                return null;
+        }
+    }
+
+    public static Set<AclEntryFlag> decodeAclFlags(int aclFlag) {
+        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);
+        }
+
+        return flags;
+    }
+
+    public static Set<AclEntryPermission> decodeAclMask(int aclMask) {
+        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);
+        }
+
+        return mask;
+    }
+
+    public static <B extends Buffer> B writeACLs(B buffer, int version, Collection<? extends AclEntry> acl) {
+        int lenPos = buffer.wpos();
+        buffer.putInt(0);   // length placeholder
+        buffer = encodeACLs(buffer, version, acl);
+        BufferUtils.updateLengthPlaceholder(buffer, lenPos);
+        return buffer;
+    }
+
+    public static <B extends Buffer> B encodeACLs(B buffer, int version, Collection<? extends AclEntry> acl) {
+        Objects.requireNonNull(acl, "No ACL");
+        if (version >= SftpConstants.SFTP_V6) {
+            buffer.putInt(0);   // TODO handle ACL flags
+        }
+
+        int numEntries = GenericUtils.size(acl);
+        buffer.putInt(numEntries);
+        if (numEntries > 0) {
+            for (AclEntry e : acl) {
+                buffer = writeAclEntry(buffer, e);
+            }
+        }
+
+        return buffer;
+    }
+
+    public static <B extends Buffer> B writeAclEntry(B buffer, AclEntry acl) {
+        Objects.requireNonNull(acl, "No ACL");
+
+        AclEntryType type = acl.type();
+        int aclType = encodeAclEntryType(type);
+        ValidateUtils.checkTrue(aclType >= 0, "Unknown ACL type: %s", type);
+        buffer.putInt(aclType);
+        buffer.putInt(encodeAclFlags(acl.flags()));
+        buffer.putInt(encodeAclMask(acl.permissions()));
+
+        Principal user = acl.principal();
+        buffer.putString(user.getName());
+        return buffer;
+    }
+
+    /**
+     * Returns the equivalent SFTP value for the ACL type
+     *
+     * @param type The {@link AclEntryType}
+     * @return The equivalent {@code ACE_SYSTEM_xxx_TYPE} or negative
+     * if {@code null} or unknown type
+     */
+    public static int encodeAclEntryType(AclEntryType type) {
+        if (type == null) {
+            return Integer.MIN_VALUE;
+        }
+
+        switch(type) {
+            case ALARM:
+                return SftpConstants.ACE4_SYSTEM_ALARM_ACE_TYPE;
+            case ALLOW:
+                return SftpConstants.ACE4_ACCESS_ALLOWED_ACE_TYPE;
+            case AUDIT:
+                return SftpConstants.ACE4_SYSTEM_AUDIT_ACE_TYPE;
+            case DENY:
+                return SftpConstants.ACE4_ACCESS_DENIED_ACE_TYPE;
+            default:
+                return -1;
+        }
+    }
+
+    public static long encodeAclFlags(Collection<AclEntryFlag> flags) {
+        if (GenericUtils.isEmpty(flags)) {
+            return 0L;
+        }
+
+        long aclFlag = 0L;
+        if (flags.contains(AclEntryFlag.FILE_INHERIT)) {
+            aclFlag |= SftpConstants.ACE4_FILE_INHERIT_ACE;
+        }
+        if (flags.contains(AclEntryFlag.DIRECTORY_INHERIT)) {
+            aclFlag |= SftpConstants.ACE4_DIRECTORY_INHERIT_ACE;
+        }
+        if (flags.contains(AclEntryFlag.NO_PROPAGATE_INHERIT)) {
+            aclFlag |= SftpConstants.ACE4_NO_PROPAGATE_INHERIT_ACE;
+        }
+        if (flags.contains(AclEntryFlag.INHERIT_ONLY)) {
+            aclFlag |= SftpConstants.ACE4_INHERIT_ONLY_ACE;
+        }
+
+        return aclFlag;
+    }
+
+    public static long encodeAclMask(Collection<AclEntryPermission> mask) {
+        if (GenericUtils.isEmpty(mask)) {
+            return 0L;
+        }
+
+        long aclMask = 0L;
+        if (mask.contains(AclEntryPermission.READ_DATA)) {
+            aclMask |= SftpConstants.ACE4_READ_DATA;
+        }
+        if (mask.contains(AclEntryPermission.LIST_DIRECTORY)) {
+            aclMask |= SftpConstants.ACE4_LIST_DIRECTORY;
+        }
+        if (mask.contains(AclEntryPermission.WRITE_DATA)) {
+            aclMask |= SftpConstants.ACE4_WRITE_DATA;
+        }
+        if (mask.contains(AclEntryPermission.ADD_FILE)) {
+            aclMask |= SftpConstants.ACE4_ADD_FILE;
+        }
+        if (mask.contains(AclEntryPermission.APPEND_DATA)) {
+            aclMask |= SftpConstants.ACE4_APPEND_DATA;
+        }
+        if (mask.contains(AclEntryPermission.ADD_SUBDIRECTORY)) {
+            aclMask |= SftpConstants.ACE4_ADD_SUBDIRECTORY;
+        }
+        if (mask.contains(AclEntryPermission.READ_NAMED_ATTRS)) {
+            aclMask |= SftpConstants.ACE4_READ_NAMED_ATTRS;
+        }
+        if (mask.contains(AclEntryPermission.WRITE_NAMED_ATTRS)) {
+            aclMask |= SftpConstants.ACE4_WRITE_NAMED_ATTRS;
+        }
+        if (mask.contains(AclEntryPermission.EXECUTE)) {
+            aclMask |= SftpConstants.ACE4_EXECUTE;
+        }
+        if (mask.contains(AclEntryPermission.DELETE_CHILD)) {
+            aclMask |= SftpConstants.ACE4_DELETE_CHILD;
+        }
+        if (mask.contains(AclEntryPermission.READ_ATTRIBUTES)) {
+            aclMask |= SftpConstants.ACE4_READ_ATTRIBUTES;
+        }
+        if (mask.contains(AclEntryPermission.WRITE_ATTRIBUTES)) {
+            aclMask |= SftpConstants.ACE4_WRITE_ATTRIBUTES;
+        }
+        if (mask.contains(AclEntryPermission.DELETE)) {
+            aclMask |= SftpConstants.ACE4_DELETE;
+        }
+        if (mask.contains(AclEntryPermission.READ_ACL)) {
+            aclMask |= SftpConstants.ACE4_READ_ACL;
+        }
+        if (mask.contains(AclEntryPermission.WRITE_ACL)) {
+            aclMask |= SftpConstants.ACE4_WRITE_ACL;
+        }
+        if (mask.contains(AclEntryPermission.WRITE_OWNER)) {
+            aclMask |= SftpConstants.ACE4_WRITE_OWNER;
+        }
+        if (mask.contains(AclEntryPermission.SYNCHRONIZE)) {
+            aclMask |= SftpConstants.ACE4_SYNCHRONIZE;
+        }
+
+        return aclMask;
+    }
+
+    /**
+     * Encodes a {@link FileTime} value into a buffer
+     *
+     * @param <B> Type of {@link Buffer} being updated
+     * @param buffer The target buffer instance
+     * @param version The encoding version
+     * @param flags The encoding flags
+     * @param time The value to encode
+     * @return The updated buffer
+     */
+    public static <B extends Buffer> B writeTime(B buffer, int version, int flags, FileTime time) {
+        // for v3 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#page-8
+        // for v6 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-16
+        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));
+        }
+
+        return buffer;
+    }
+
+    /**
+     * 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) {
+        // for v3 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#page-8
+        // for v6 see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-16
+        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);
+    }
+
+    /**
+     * Creates an &quot;ls -l&quot; compatible long name string
+     *
+     * @param shortName The short file name - can also be &quot;.&quot; or &quot;..&quot;
+     * @param attributes The file's attributes - e.g., size, owner, permissions, etc.
+     * @return A {@link String} representing the &quot;long&quot; file name as per
+     * <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02">SFTP version 3 - section 7</A>
+     */
+    public static String getLongName(String shortName, Map<String, ?> attributes) {
+        String owner = Objects.toString(attributes.get("owner"), null);
+        String username = OsUtils.getCanonicalUser(owner);
+        if (GenericUtils.isEmpty(username)) {
+            username = SftpUniversalOwnerAndGroup.Owner.getName();
+        }
+
+        String group = Objects.toString(attributes.get("group"), null);
+        group = OsUtils.resolveCanonicalGroup(group, owner);
+        if (GenericUtils.isEmpty(group)) {
+            group = SftpUniversalOwnerAndGroup.Group.getName();
+        }
+
+        Number length = (Number) attributes.get("size");
+        if (length == null) {
+            length = 0L;
+        }
+
+        String lengthString = String.format("%1$8s", length);
+        String linkCount = Objects.toString(attributes.get("nlink"), null);
+        if (GenericUtils.isEmpty(linkCount)) {
+            linkCount = "1";
+        }
+
+        Boolean isDirectory = (Boolean) attributes.get("isDirectory");
+        Boolean isLink = (Boolean) attributes.get("isSymbolicLink");
+        @SuppressWarnings("unchecked")
+        Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
+        if (perms == null) {
+            perms = EnumSet.noneOf(PosixFilePermission.class);
+        }
+        String permsString = PosixFilePermissions.toString(perms);
+        String timeStamp = UnixDateFormat.getUnixDate((FileTime) attributes.get("lastModifiedTime"));
+        StringBuilder sb = new StringBuilder(
+                GenericUtils.length(linkCount) + GenericUtils.length(username) + GenericUtils.length(group)
+              + GenericUtils.length(timeStamp) + GenericUtils.length(lengthString)
+              + GenericUtils.length(permsString) + GenericUtils.length(shortName)
+              + Integer.SIZE);
+        sb.append(SftpHelper.getBool(isDirectory) ? 'd' : (SftpHelper.getBool(isLink) ? 'l' : '-')).append(permsString);
+
+        sb.append(' ');
+        for (int index = linkCount.length(); index < 3; index++) {
+            sb.append(' ');
+        }
+        sb.append(linkCount);
+
+        sb.append(' ').append(username);
+        for (int index = username.length(); index < 8; index++) {
+            sb.append(' ');
+        }
+
+        sb.append(' ').append(group);
+        for (int index = group.length(); index < 8; index++) {
+            sb.append(' ');
+        }
+
+        sb.append(' ').append(lengthString).append(' ').append(timeStamp).append(' ').append(shortName);
+        return sb.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroup.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroup.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroup.java
new file mode 100644
index 0000000..68ab055
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpUniversalOwnerAndGroup.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.apache.sshd.common.NamedResource;
+
+/**
+ * Some universal identifiers used in owner and/or group specification strings
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#page-12">SFTP ACL</A>
+ */
+public enum SftpUniversalOwnerAndGroup implements NamedResource {
+    Owner,          // The owner of the file.
+    Group,          // The group associated with the file.
+    Everyone,       // The world.
+    Interactive,    // Accessed from an interactive terminal.
+    Network,        // Accessed via the network.
+    Dialup,         // Accessed as a dialup user to the server.
+    Batch,          // Accessed from a batch job.
+    Anonymous,      // Accessed without any authentication.
+    Authenticated,  // Any authenticated user (opposite of ANONYMOUS).
+    Service;        // Access from a system service.
+
+    public static final Set<SftpUniversalOwnerAndGroup> VALUES =
+            Collections.unmodifiableSet(EnumSet.allOf(SftpUniversalOwnerAndGroup.class));
+
+    private final String name;
+
+    SftpUniversalOwnerAndGroup() {
+        name = name().toUpperCase() + "@";
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    public static SftpUniversalOwnerAndGroup fromName(String name) {
+        return NamedResource.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
new file mode 100644
index 0000000..15cb3fe
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.subsystem.sftp.extensions;
+
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @param <T> Parse result type
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractParser<T> implements ExtensionParser<T> {
+    private final String name;
+
+    protected AbstractParser(String name) {
+        this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name");
+    }
+
+    @Override
+    public final String getName() {
+        return name;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AclSupportedParser.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AclSupportedParser.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AclSupportedParser.java
new file mode 100644
index 0000000..ea2d12d
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AclSupportedParser.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.subsystem.sftp.extensions;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.AclSupportedParser.AclCapabilities;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.logging.LoggingUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class AclSupportedParser extends AbstractParser<AclCapabilities> {
+    /**
+     * The &quot;acl-supported&quot; information as per
+     * <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-11">DRAFT 11 - section 5.4</A>
+     *
+     * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+     */
+    public static class AclCapabilities implements Serializable, Cloneable {
+        private static final long serialVersionUID = -3118426327336468237L;
+        private int capabilities;
+
+        public AclCapabilities() {
+            this(0);
+        }
+
+        public AclCapabilities(int capabilities) {
+            this.capabilities = capabilities;
+        }
+
+        public int getCapabilities() {
+            return capabilities;
+        }
+
+        public void setCapabilities(int capabilities) {
+            this.capabilities = capabilities;
+        }
+
+        @Override
+        public int hashCode() {
+            return getCapabilities();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (obj == this) {
+                return true;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+
+            return ((AclCapabilities) obj).getCapabilities() == getCapabilities();
+        }
+
+        @Override
+        public AclCapabilities clone() {
+            try {
+                return getClass().cast(super.clone());
+            } catch (CloneNotSupportedException e) {
+                throw new RuntimeException("Failed to clone " + toString() + ": " + e.getMessage(), e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return Objects.toString(decodeAclCapabilities(getCapabilities()));
+        }
+
+        private static class LazyAclCapabilityNameHolder {
+            private static final String ACL_CAP_NAME_PREFIX = "SSH_ACL_CAP_";
+            private static final Map<Integer, String> ACL_VALUES_MAP = LoggingUtils.generateMnemonicMap(SftpConstants.class, ACL_CAP_NAME_PREFIX);
+            private static final Map<String, Integer> ACL_NAMES_MAP =
+                    Collections.unmodifiableMap(GenericUtils.flipMap(ACL_VALUES_MAP, GenericUtils.caseInsensitiveMap(), false));
+        }
+
+        @SuppressWarnings("synthetic-access")
+        public static Map<String, Integer> getAclCapabilityNamesMap() {
+            return LazyAclCapabilityNameHolder.ACL_NAMES_MAP;
+        }
+
+        /**
+         * @param name The ACL capability name - may be without the &quot;SSH_ACL_CAP_xxx&quot; prefix.
+         * Ignored if {@code null}/empty
+         * @return The matching {@link Integer} value - or {@code null} if no match found
+         */
+        public static Integer getAclCapabilityValue(String name) {
+            if (GenericUtils.isEmpty(name)) {
+                return null;
+            }
+
+            name = name.toUpperCase();
+            if (!name.startsWith(LazyAclCapabilityNameHolder.ACL_CAP_NAME_PREFIX)) {
+                name += LazyAclCapabilityNameHolder.ACL_CAP_NAME_PREFIX;
+            }
+
+            Map<String, Integer> map = getAclCapabilityNamesMap();
+            return map.get(name);
+        }
+
+        @SuppressWarnings("synthetic-access")
+        public static Map<Integer, String> getAclCapabilityValuesMap() {
+            return LazyAclCapabilityNameHolder.ACL_VALUES_MAP;
+        }
+
+        public static String getAclCapabilityName(int aclCapValue) {
+            Map<Integer, String> map = getAclCapabilityValuesMap();
+            String name = map.get(aclCapValue);
+            if (GenericUtils.isEmpty(name)) {
+                return Integer.toString(aclCapValue);
+            } else {
+                return name;
+            }
+        }
+
+        public static Set<String> decodeAclCapabilities(int mask) {
+            if (mask == 0) {
+                return Collections.emptySet();
+            }
+
+            Set<String> caps = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+            Map<Integer, String> map = getAclCapabilityValuesMap();
+            map.forEach((value, name) -> {
+                if ((mask & value) != 0) {
+                    caps.add(name);
+                }
+            });
+
+            return caps;
+        }
+
+        public static int constructAclCapabilities(Collection<Integer> maskValues) {
+            if (GenericUtils.isEmpty(maskValues)) {
+                return 0;
+            }
+
+            int mask = 0;
+            for (Integer v : maskValues) {
+                mask |= v;
+            }
+
+            return mask;
+        }
+
+        public static Set<Integer> deconstructAclCapabilities(int mask) {
+            if (mask == 0) {
+                return Collections.emptySet();
+            }
+
+            Map<Integer, String> map = getAclCapabilityValuesMap();
+            Set<Integer> caps = new HashSet<>(map.size());
+            for (Integer v : map.keySet()) {
+                if ((mask & v) != 0) {
+                    caps.add(v);
+                }
+            }
+
+            return caps;
+        }
+    }
+
+    public static final AclSupportedParser INSTANCE = new AclSupportedParser();
+
+    public AclSupportedParser() {
+        super(SftpConstants.EXT_ACL_SUPPORTED);
+    }
+
+    @Override
+    public AclCapabilities parse(byte[] input, int offset, int len) {
+        return parse(new ByteArrayBuffer(input, offset, len));
+    }
+
+    public AclCapabilities parse(Buffer buffer) {
+        return new AclCapabilities(buffer.getInt());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
new file mode 100644
index 0000000..cd74a26
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.subsystem.sftp.extensions;
+
+import java.util.function.Function;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.util.NumberUtils;
+
+/**
+ * @param <T> Result type
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ExtensionParser<T> extends NamedResource, Function<byte[], T> {
+    default T parse(byte[] input) {
+        return parse(input, 0, NumberUtils.length(input));
+    }
+
+    T parse(byte[] input, int offset, int len);
+
+    @Override
+    default T apply(byte[] input) {
+        return parse(input);
+    }
+}