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 "ls -l" compatible long name string
+ *
+ * @param shortName The short file name - can also be "." or ".."
+ * @param attributes The file's attributes - e.g., size, owner, permissions, etc.
+ * @return A {@link String} representing the "long" 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 "acl-supported" 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 "SSH_ACL_CAP_xxx" 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);
+ }
+}