/*
 * 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.sftp.common;

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.time.DateTimeException;
import java.time.Instant;
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 java.util.regex.Pattern;

import org.apache.sshd.common.PropertyResolver;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
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.common.util.io.IoUtils;
import org.apache.sshd.sftp.SftpModuleProperties;
import org.apache.sshd.sftp.client.SftpClient.Attribute;
import org.apache.sshd.sftp.client.SftpClient.Attributes;
import org.apache.sshd.sftp.server.DefaultGroupPrincipal;
import org.apache.sshd.sftp.server.InvalidHandleException;
import org.apache.sshd.sftp.server.UnixDateFormat;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class SftpHelper {

    public static final Map<Integer, String> DEFAULT_SUBSTATUS_MESSAGE;

    static {
        Map<Integer, String> map = new TreeMap<>(Comparator.naturalOrder());
        map.put(SftpConstants.SSH_FX_OK, "Success");
        map.put(SftpConstants.SSH_FX_EOF, "End of file");
        map.put(SftpConstants.SSH_FX_NO_SUCH_FILE, "No such file or directory");
        map.put(SftpConstants.SSH_FX_PERMISSION_DENIED, "Permission denied");
        map.put(SftpConstants.SSH_FX_FAILURE, "General failure");
        map.put(SftpConstants.SSH_FX_BAD_MESSAGE, "Bad message data");
        map.put(SftpConstants.SSH_FX_NO_CONNECTION, "No connection to server");
        map.put(SftpConstants.SSH_FX_CONNECTION_LOST, "Connection lost");
        map.put(SftpConstants.SSH_FX_OP_UNSUPPORTED, "Unsupported operation requested");
        map.put(SftpConstants.SSH_FX_INVALID_HANDLE, "Invalid handle value");
        map.put(SftpConstants.SSH_FX_NO_SUCH_PATH, "No such path");
        map.put(SftpConstants.SSH_FX_FILE_ALREADY_EXISTS, "File/Directory already exists");
        map.put(SftpConstants.SSH_FX_WRITE_PROTECT, "File/Directory is write-protected");
        map.put(SftpConstants.SSH_FX_NO_MEDIA, "No such meadia");
        map.put(SftpConstants.SSH_FX_NO_SPACE_ON_FILESYSTEM, "No space left on device");
        map.put(SftpConstants.SSH_FX_QUOTA_EXCEEDED, "Quota exceeded");
        map.put(SftpConstants.SSH_FX_UNKNOWN_PRINCIPAL, "Unknown user/group");
        map.put(SftpConstants.SSH_FX_LOCK_CONFLICT, "Lock conflict");
        map.put(SftpConstants.SSH_FX_DIR_NOT_EMPTY, "Directory not empty");
        map.put(SftpConstants.SSH_FX_NOT_A_DIRECTORY, "Accessed location is not a directory");
        map.put(SftpConstants.SSH_FX_INVALID_FILENAME, "Invalid filename");
        map.put(SftpConstants.SSH_FX_LINK_LOOP, "Link loop");
        map.put(SftpConstants.SSH_FX_CANNOT_DELETE, "Cannot remove");
        map.put(SftpConstants.SSH_FX_INVALID_PARAMETER, "Invalid parameter");
        map.put(SftpConstants.SSH_FX_FILE_IS_A_DIRECTORY, "Accessed location is a directory");
        map.put(SftpConstants.SSH_FX_BYTE_RANGE_LOCK_CONFLICT, "Range lock conflict");
        map.put(SftpConstants.SSH_FX_BYTE_RANGE_LOCK_REFUSED, "Range lock refused");
        map.put(SftpConstants.SSH_FX_DELETE_PENDING, "Delete pending");
        map.put(SftpConstants.SSH_FX_FILE_CORRUPT, "Corrupted file/directory");
        map.put(SftpConstants.SSH_FX_OWNER_INVALID, "Invalid file/directory owner");
        map.put(SftpConstants.SSH_FX_GROUP_INVALID, "Invalid file/directory group");
        map.put(SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK, "No matching byte range lock");
        DEFAULT_SUBSTATUS_MESSAGE = Collections.unmodifiableMap(map);
    }

    // Regular expression for a plausibility check in isUnixPermissions. It requires at least two "rwx" triples,
    // but the "x" position may actually be any character (could be s, S, t, T, or any vendor-specific extension.
    //
    // Moreover, Win32-OpenSSH uses '*' for permissions not applicable on Windows.
    private static final Pattern UNIX_PERMISSIONS_START = Pattern.compile("[-dlcbps][-r][-w][-a-zA-Z*][-r*][-w*][-a-zA-Z*].*");

    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                   SftpModuleProperties#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 (!SftpModuleProperties.APPEND_END_OF_LIST_INDICATOR.getRequired(resolver)) {
            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(IoUtils.REGFILE_VIEW_ATTR));
        boolean isDir = getBool((Boolean) attributes.get(IoUtils.DIRECTORY_VIEW_ATTR));
        boolean isLnk = getBool((Boolean) attributes.get(IoUtils.SYMLINK_VIEW_ATTR));
        @SuppressWarnings("unchecked")
        Collection<PosixFilePermission> perms = (Collection<PosixFilePermission>) attributes.get(IoUtils.PERMISSIONS_VIEW_ATTR);
        Number size = (Number) attributes.get(IoUtils.SIZE_VIEW_ATTR);
        FileTime lastModifiedTime = (FileTime) attributes.get(IoUtils.LASTMOD_TIME_VIEW_ATTR);
        FileTime lastAccessTime = (FileTime) attributes.get(IoUtils.LASTACC_TIME_VIEW_ATTR);
        Map<?, ?> extensions = (Map<?, ?>) attributes.get(IoUtils.EXTENDED_VIEW_ATTR);
        int flags = ((isReg || isLnk) && (size != null) ? SftpConstants.SSH_FILEXFER_ATTR_SIZE : 0)
                    | (attributes.containsKey(IoUtils.USERID_VIEW_ATTR) && attributes.containsKey(IoUtils.GROUPID_VIEW_ATTR)
                            ? 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(IoUtils.USERID_VIEW_ATTR)).intValue());
            buffer.putInt(((Number) attributes.get(IoUtils.GROUPID_VIEW_ATTR)).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(IoUtils.REGFILE_VIEW_ATTR));
        boolean isDir = getBool((Boolean) attributes.get(IoUtils.DIRECTORY_VIEW_ATTR));
        boolean isLnk = getBool((Boolean) attributes.get(IoUtils.SYMLINK_VIEW_ATTR));
        @SuppressWarnings("unchecked")
        Collection<PosixFilePermission> perms = (Collection<PosixFilePermission>) attributes.get(IoUtils.PERMISSIONS_VIEW_ATTR);
        Number size = (Number) attributes.get(IoUtils.SIZE_VIEW_ATTR);
        FileTime lastModifiedTime = (FileTime) attributes.get(IoUtils.LASTMOD_TIME_VIEW_ATTR);
        FileTime lastAccessTime = (FileTime) attributes.get(IoUtils.LASTACC_TIME_VIEW_ATTR);
        FileTime creationTime = (FileTime) attributes.get(IoUtils.CREATE_TIME_VIEW_ATTR);
        @SuppressWarnings("unchecked")
        Collection<AclEntry> acl = (Collection<AclEntry>) attributes.get(IoUtils.ACL_VIEW_ATTR);
        Map<?, ?> extensions = (Map<?, ?>) attributes.get(IoUtils.EXTENDED_VIEW_ATTR);
        int flags = (((isReg || isLnk) && (size != null)) ? SftpConstants.SSH_FILEXFER_ATTR_SIZE : 0)
                    | ((attributes.containsKey(IoUtils.OWNER_VIEW_ATTR) && attributes.containsKey(IoUtils.GROUP_VIEW_ATTR))
                            ? 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(IoUtils.OWNER_VIEW_ATTR), SftpUniversalOwnerAndGroup.Owner.getName()));
            buffer.putString(
                    Objects.toString(attributes.get(IoUtils.GROUP_VIEW_ATTR), 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, creationTime);
        }
        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;
    }

    public static <B extends Buffer> B writeAttributes(B buffer, Attributes attributes, int sftpVersion) {
        int flagsMask = 0;
        Collection<Attribute> flags = Objects.requireNonNull(attributes, "No attributes").getFlags();
        if (sftpVersion == SftpConstants.SFTP_V3) {
            for (Attribute a : flags) {
                switch (a) {
                    case Size:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
                        break;
                    case UidGid:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
                        break;
                    case Perms:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
                        break;
                    case AccessTime:
                        if (flags.contains(Attribute.ModifyTime)) {
                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
                        }
                        break;
                    case ModifyTime:
                        if (flags.contains(Attribute.AccessTime)) {
                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
                        }
                        break;
                    case Extensions:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
                        break;
                    default: // do nothing
                }
            }
            buffer.putInt(flagsMask);
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
                buffer.putLong(attributes.getSize());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
                buffer.putInt(attributes.getUserId());
                buffer.putInt(attributes.getGroupId());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
                buffer.putInt(attributes.getPermissions());
            }

            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
                buffer = writeTime(buffer, sftpVersion, flagsMask, attributes.getAccessTime());
                buffer = writeTime(buffer, sftpVersion, flagsMask, attributes.getModifyTime());
            }
        } else if (sftpVersion >= SftpConstants.SFTP_V4) {
            for (Attribute a : flags) {
                switch (a) {
                    case Size:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
                        break;
                    case OwnerGroup: {
                        /*
                         * According to
                         * https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt
                         * section 7.5
                         *
                         * If either the owner or group field is zero length, the field should be considered absent, and no
                         * change should be made to that specific field during a modification operation.
                         */
                        String owner = attributes.getOwner();
                        String group = attributes.getGroup();
                        if (GenericUtils.isNotEmpty(owner) && GenericUtils.isNotEmpty(group)) {
                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
                        }
                        break;
                    }
                    case Perms:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
                        break;
                    case AccessTime:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME;
                        break;
                    case ModifyTime:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME;
                        break;
                    case CreateTime:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_CREATETIME;
                        break;
                    case Acl:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACL;
                        break;
                    case Extensions:
                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
                        break;
                    default: // do nothing
                }
            }
            buffer.putInt(flagsMask);
            buffer.putByte((byte) attributes.getType());
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
                buffer.putLong(attributes.getSize());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
                String owner = attributes.getOwner();
                buffer.putString(owner);

                String group = attributes.getGroup();
                buffer.putString(group);
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
                buffer.putInt(attributes.getPermissions());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
                buffer = writeTime(buffer, sftpVersion, flagsMask, attributes.getAccessTime());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
                buffer = writeTime(buffer, sftpVersion, flagsMask, attributes.getCreateTime());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
                buffer = writeTime(buffer, sftpVersion, flagsMask, attributes.getModifyTime());
            }
            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
                buffer = writeACLs(buffer, sftpVersion, attributes.getAcl());
            }

            // TODO: for v5 ? 6? add CTIME (see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-16 - v6)
        } else {
            throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + sftpVersion);
        }

        if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
            buffer = writeExtensions(buffer, attributes.getExtensions());
        }

        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) {
        switch (perms & SftpConstants.S_IFMT) {
            case SftpConstants.S_IFLNK:
                return SftpConstants.SSH_FILEXFER_TYPE_SYMLINK;
            case SftpConstants.S_IFREG:
                return SftpConstants.SSH_FILEXFER_TYPE_REGULAR;
            case SftpConstants.S_IFDIR:
                return SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY;
            case SftpConstants.S_IFSOCK:
                return SftpConstants.SSH_FILEXFER_TYPE_SOCKET;
            case SftpConstants.S_IFBLK:
                return SftpConstants.SSH_FILEXFER_TYPE_BLOCK_DEVICE;
            case SftpConstants.S_IFCHR:
                return SftpConstants.SSH_FILEXFER_TYPE_CHAR_DEVICE;
            case SftpConstants.S_IFIFO:
                return SftpConstants.SSH_FILEXFER_TYPE_FIFO;
            default:
                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;
        }
    }

    /**
     * Converts a POSIX/Linux file type indicator (as if obtained by "ls -l") to a file type.
     *
     * @param  ch character to convert
     * @return    the file type
     */
    public static int fileTypeFromChar(char ch) {
        switch (ch) {
            case '-':
                return SftpConstants.SSH_FILEXFER_TYPE_REGULAR;
            case 'd':
                return SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY;
            case 'l':
                return SftpConstants.SSH_FILEXFER_TYPE_SYMLINK;
            case 's':
                return SftpConstants.SSH_FILEXFER_TYPE_SOCKET;
            case 'b':
                return SftpConstants.SSH_FILEXFER_TYPE_BLOCK_DEVICE;
            case 'c':
                return SftpConstants.SSH_FILEXFER_TYPE_CHAR_DEVICE;
            case 'p':
                return SftpConstants.SSH_FILEXFER_TYPE_FIFO;
            default:
                return SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN;
        }
    }

    /**
     * Fills in missing information in the attributes if an SFTP v3 long name is available. If missing information
     * cannot be extracted from the long name, it is not filled in, but no error or exception is generated.
     * <p>
     * The SFTP draft RFC discourages parsing a long name to extract information and states the attributes should be
     * used instead. But some SFTP v3 servers do not send all information in the attributes... for instance the
     * SolarWinds SFTP server on Windows does not include the file type flags in the permissions. The only way to
     * determine the file type is then to look at the permissions string in the long name.
     * </p>
     *
     * @param  attrs    {@link Attributes} to complete, if necessary
     * @param  longName to use to find missing information, may be {@code null} or empty.
     * @return          {@code attrs}
     */
    public static Attributes complete(Attributes attrs, String longName) {
        if (longName == null || longName.isEmpty()) {
            return attrs;
        }
        if (attrs.getType() == SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN //
                && (attrs.getPermissions() & SftpConstants.S_IFMT) == 0 //
                && isUnixPermissions(longName)) {
            // Some SFTP v3 servers do not send the file type flags in the permissions. The draft RFC does not
            // explicitly say they should be included... if we have a longname, it's SFTP v3, and it should start
            // with the permissions string as in POSIX/Linux "ls -l". The first character determines the file type.
            int type = fileTypeFromChar(longName.charAt(0));
            if (type != SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN) {
                attrs.setType(type);
                attrs.setPermissions(attrs.getPermissions() | fileTypeToPermission(type));
            }
        }
        return attrs;
    }

    private static boolean isUnixPermissions(String longName) {
        // Some plausibility checks. The SFTP draft RFC only gives a recommended format for "longname",
        // not all SFTP servers might follow it.
        int i = longName.indexOf(' ');
        if (i < 6 || i > 11) {
            // POSIX permissions should be 10 characters. However, sometimes there may be an additional character,
            // like '@' on OS X to indicate extended permissions. So we allow 11. We also don't require the full
            // 9 characters for user-group-others; at least the SolarWind SFTP server for Windows has a bug an omits
            // the executable flag for 'others'. So be generous and require at least 7 characters (one file type,
            // and at least two triplets).
            return false;
        }
        return UNIX_PERMISSIONS_START.matcher(longName.substring(0, i)).matches();
    }

    /**
     * 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(IoUtils.REGFILE_VIEW_ATTR, Boolean.TRUE);
                    break;
                case SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY:
                    attrs.put(IoUtils.DIRECTORY_VIEW_ATTR, Boolean.TRUE);
                    break;
                case SftpConstants.SSH_FILEXFER_TYPE_SYMLINK:
                    attrs.put(IoUtils.SYMLINK_VIEW_ATTR, 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(IoUtils.OTHERFILE_VIEW_ATTR, Boolean.TRUE);
                    break;
                default: // ignored
            }
        }

        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
            attrs.put(IoUtils.SIZE_VIEW_ATTR, buffer.getLong());
        }

        if (version == SftpConstants.SFTP_V3) {
            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
                attrs.put(IoUtils.USERID_VIEW_ATTR, buffer.getInt());
                attrs.put(IoUtils.GROUPID_VIEW_ATTR, 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(IoUtils.OWNER_VIEW_ATTR, new DefaultGroupPrincipal(buffer.getString()));
                attrs.put(IoUtils.GROUP_VIEW_ATTR, new DefaultGroupPrincipal(buffer.getString()));
            }
        }

        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
            attrs.put(IoUtils.PERMISSIONS_VIEW_ATTR, permissionsToAttributes(buffer.getInt()));
        }

        if (version == SftpConstants.SFTP_V3) {
            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
                attrs.put(IoUtils.LASTACC_TIME_VIEW_ATTR, readTime(buffer, version, flags));
                attrs.put(IoUtils.LASTMOD_TIME_VIEW_ATTR, readTime(buffer, version, flags));
            }
        } else if (version >= SftpConstants.SFTP_V4) {
            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
                attrs.put(IoUtils.LASTACC_TIME_VIEW_ATTR, readTime(buffer, version, flags));
            }
            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
                attrs.put(IoUtils.CREATE_TIME_VIEW_ATTR, readTime(buffer, version, flags));
            }
            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
                attrs.put(IoUtils.LASTMOD_TIME_VIEW_ATTR, readTime(buffer, version, flags));
            }
            // modification time sub-seconds
            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(IoUtils.ACL_VIEW_ATTR, 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(IoUtils.EXTENDED_VIEW_ATTR, readExtensions(buffer));
        }

        return attrs;
    }

    public static NavigableMap<String, byte[]> readExtensions(Buffer buffer) {
        int count = buffer.getInt();
        // Protect against malicious or malformed packets
        if ((count < 0) || (count > SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT)) {
            throw new IndexOutOfBoundsException("Illogical extensions count: " + count);
        }

        // NOTE
        NavigableMap<String, byte[]> extended = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (int i = 1; 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 = MapEntryUtils.size(extensions);
        buffer.putUInt(numExtensions);
        if (numExtensions <= 0) {
            return buffer;
        }

        for (Map.Entry<?, ?> ee : extensions.entrySet()) {
            Object key = Objects.requireNonNull(ee.getKey(), "No extension type");
            Object value = Objects.requireNonNull(ee.getValue(), "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 (MapEntryUtils.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);
        for (Map.Entry<?, ?> ee : extensions.entrySet()) {
            Object key = Objects.requireNonNull(ee.getKey(), "No extension type");
            Object value = ValidateUtils.checkNotNull(ee.getValue(), "No value for extension=%s", key);
            String prev = map.put(key.toString(),
                    (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 (MapEntryUtils.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();
        // Protect against malicious or malformed packets
        if ((aclSize < 0) || (aclSize > (2 * SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT))) {
            throw new IndexOutOfBoundsException("Illogical ACL entries size: " + aclSize);
        }

        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 several hundreds +
         * protect against malicious or corrupted packets
         */
        if ((count < 0) || (count > SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT)) {
            throw new IndexOutOfBoundsException("Illogical ACL entries count: " + count);
        }

        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 = 1; 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<AclEntry> acl) {
        int lenPos = buffer.wpos();
        buffer.putUInt(0L); // length placeholder
        encodeACLs(buffer, version, acl);
        BufferUtils.updateLengthPlaceholder(buffer, lenPos);
        return buffer;
    }

    public static <B extends Buffer> B encodeACLs(B buffer, int version, Collection<AclEntry> acl) {
        Objects.requireNonNull(acl, "No ACL");
        if (version >= SftpConstants.SFTP_V6) {
            buffer.putUInt(0L); // TODO handle ACL flags
        }

        int numEntries = GenericUtils.size(acl);
        buffer.putInt(numEntries);
        if (numEntries > 0) {
            for (AclEntry e : acl) {
                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) {
                buffer.putInt(time.toInstant().getNano());
            }
        } 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;
        if (version >= SftpConstants.SFTP_V4) {
            secs = buffer.getLong();
            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
                long nanos = buffer.getUInt();
                if (nanos != 0) {
                    try {
                        return FileTime.from(Instant.ofEpochSecond(secs, nanos));
                    } catch (DateTimeException | ArithmeticException e) {
                        // Beyond Instant range; drop nanos
                    }
                }
            }
        } else {
            secs = buffer.getUInt();
        }
        return FileTime.from(secs, TimeUnit.SECONDS);
    }

    /**
     * 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(IoUtils.OWNER_VIEW_ATTR), null);
        String username = OsUtils.getCanonicalUser(owner);
        if (GenericUtils.isEmpty(username)) {
            username = SftpUniversalOwnerAndGroup.Owner.getName();
        }

        String group = Objects.toString(attributes.get(IoUtils.GROUP_VIEW_ATTR), null);
        group = OsUtils.resolveCanonicalGroup(group, owner);
        if (GenericUtils.isEmpty(group)) {
            group = SftpUniversalOwnerAndGroup.Group.getName();
        }

        Number length = (Number) attributes.get(IoUtils.SIZE_VIEW_ATTR);
        if (length == null) {
            length = 0L;
        }

        String lengthString = String.format("%1$8s", length);
        String linkCount = Objects.toString(attributes.get(IoUtils.NUMLINKS_VIEW_ATTR), null);
        if (GenericUtils.isEmpty(linkCount)) {
            linkCount = "1";
        }

        Boolean isDirectory = (Boolean) attributes.get(IoUtils.DIRECTORY_VIEW_ATTR);
        Boolean isLink = (Boolean) attributes.get(IoUtils.SYMLINK_VIEW_ATTR);
        @SuppressWarnings("unchecked")
        Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get(IoUtils.PERMISSIONS_VIEW_ATTR);
        if (perms == null) {
            perms = EnumSet.noneOf(PosixFilePermission.class);
        }
        String permsString = PosixFilePermissions.toString(perms);
        String timeStamp = UnixDateFormat.getUnixDate((FileTime) attributes.get(IoUtils.LASTMOD_TIME_VIEW_ATTR));
        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();
    }
}
