| /* |
| * 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 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); |
| } |
| |
| 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) { |
| 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(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 "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(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(); |
| } |
| } |