This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 39a0841e0c55101d813f658e65c584bb42fef942 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Mon Aug 17 10:18:13 2020 +0300 [SSHD-1005] Create consistent SCP command details hierarchy --- .../org/apache/sshd/common/util/GenericUtils.java | 28 ++++++- .../apache/sshd/scp/client/DefaultScpClient.java | 5 +- .../sshd/scp/client/DefaultScpStreamResolver.java | 8 +- .../java/org/apache/sshd/scp/client/ScpClient.java | 10 ++- .../scp/client/ScpRemote2RemoteTransferHelper.java | 10 +-- .../client/ScpRemote2RemoteTransferListener.java | 10 +-- .../org/apache/sshd/scp/common/ScpFileOpener.java | 10 ++- .../java/org/apache/sshd/scp/common/ScpHelper.java | 17 +++-- .../sshd/scp/common/ScpReceiveLineHandler.java | 5 +- .../sshd/scp/common/ScpSourceStreamResolver.java | 7 +- .../sshd/scp/common/ScpTargetStreamResolver.java | 3 +- .../helpers/LocalFileScpSourceStreamResolver.java | 7 +- .../helpers/LocalFileScpTargetStreamResolver.java | 5 +- .../common/helpers/ScpDirEndCommandDetails.java | 28 +++++++ .../apache/sshd/scp/common/helpers/ScpIoUtils.java | 11 ++- .../helpers/ScpPathCommandDetailsSupport.java | 35 ++++++++- .../helpers/ScpReceiveDirCommandDetails.java | 7 +- .../ScpTimestampCommandDetails.java} | 42 ++++++++--- .../client/ScpRemote2RemoteTransferHelperTest.java | 6 +- .../helpers/AbstractScpCommandDetailsTest.java | 88 ++++++++++++++++++++++ .../server/ScpReceiveDirCommandDetailsTest.java | 49 ++++++++++++ 21 files changed, 323 insertions(+), 68 deletions(-) diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java index ea2c4e6..f48a49f 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -298,6 +298,30 @@ public final class GenericUtils { return !isEmpty(c); } + /** + * + * @param <T> Generic element type + * @param c1 First collection + * @param c2 Second collection + * @return {@code true} if the following holds: + * <UL> + * <LI>Same size - <B>Note:</B> {@code null} collections are consider equal to empty ones</LI> + * + * <LI>First collection contains all elements of second one and vice versa</LI> + * </UL> + */ + public static <T> boolean equals(Collection<T> c1, Collection<T> c2) { + if (isEmpty(c1)) { + return isEmpty(c2); + } else if (isEmpty(c2)) { + return false; + } + + return (c1.size() == c2.size()) + && c1.containsAll(c2) + && c2.containsAll(c1); + } + public static int size(Map<?, ?> m) { return (m == null) ? 0 : m.size(); } @@ -1039,7 +1063,7 @@ public final class GenericUtils { /** * Check if a duration is positive - * + * * @param d the duration * @return <code>true</code> if the duration is greater than zero */ @@ -1049,7 +1073,7 @@ public final class GenericUtils { /** * Check if a duration is negative or zero - * + * * @param d the duration * @return <code>true</code> if the duration is negative or zero */ diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java index 471ef2d..6a032e8 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java @@ -38,9 +38,9 @@ import org.apache.sshd.common.file.util.MockPath; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.scp.common.ScpFileOpener; import org.apache.sshd.scp.common.ScpHelper; -import org.apache.sshd.scp.common.ScpTimestamp; import org.apache.sshd.scp.common.ScpTransferEventListener; import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -102,7 +102,8 @@ public class DefaultScpClient extends AbstractScpClient { } @Override - public void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) + public void upload( + InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestampCommandDetails time) throws IOException { int namePos = ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified").lastIndexOf('/'); String name = (namePos < 0) diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpStreamResolver.java index fb82cf2..eb21379 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpStreamResolver.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpStreamResolver.java @@ -28,7 +28,7 @@ import java.util.Set; import org.apache.sshd.common.session.Session; import org.apache.sshd.scp.common.ScpSourceStreamResolver; -import org.apache.sshd.scp.common.ScpTimestamp; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -37,14 +37,14 @@ public class DefaultScpStreamResolver implements ScpSourceStreamResolver { private final String name; private final Path mockPath; private final Collection<PosixFilePermission> perms; - private final ScpTimestamp time; + private final ScpTimestampCommandDetails time; private final long size; private final InputStream local; private final String cmd; public DefaultScpStreamResolver( String name, Path mockPath, Collection<PosixFilePermission> perms, - ScpTimestamp time, long size, InputStream local, String cmd) { + ScpTimestampCommandDetails time, long size, InputStream local, String cmd) { this.name = name; this.mockPath = mockPath; this.perms = perms; @@ -70,7 +70,7 @@ public class DefaultScpStreamResolver implements ScpSourceStreamResolver { } @Override - public ScpTimestamp getTimestamp() throws IOException { + public ScpTimestampCommandDetails getTimestamp() throws IOException { return time; } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java index c40d07b..ef781bc 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java @@ -33,7 +33,7 @@ import org.apache.sshd.common.session.SessionHolder; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.scp.common.ScpHelper; -import org.apache.sshd.scp.common.ScpTimestamp; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -116,20 +116,22 @@ public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHo // NOTE: due to SCP command limitations, the amount of data to be uploaded must be known a-priori // To upload a dynamic amount of data use SFTP - default void upload(byte[] data, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) + default void upload(byte[] data, String remote, Collection<PosixFilePermission> perms, ScpTimestampCommandDetails time) throws IOException { upload(data, 0, data.length, remote, perms, time); } default void upload( - byte[] data, int offset, int len, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) + byte[] data, int offset, int len, String remote, Collection<PosixFilePermission> perms, + ScpTimestampCommandDetails time) throws IOException { try (InputStream local = new ByteArrayInputStream(data, offset, len)) { upload(local, remote, len, perms, time); } } - void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) + void upload( + InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestampCommandDetails time) throws IOException; static String createSendCommand(String remote, Collection<Option> options) { diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java index 1b4852a..d558576 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java @@ -35,10 +35,10 @@ import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.io.LimitInputStream; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.scp.client.ScpClient.Option; -import org.apache.sshd.scp.common.ScpTimestamp; import org.apache.sshd.scp.common.helpers.AbstractScpCommandDetails; import org.apache.sshd.scp.common.helpers.ScpIoUtils; import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * Helps transfer files between 2 servers rather than between server and local file system by using 2 @@ -133,10 +133,10 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { } char cmdName = header.charAt(0); - ScpTimestamp time = null; - if (cmdName == ScpTimestamp.COMMAND_NAME) { + ScpTimestampCommandDetails time = null; + if (cmdName == ScpTimestampCommandDetails.COMMAND_NAME) { // Pass along the "T<mtime> 0 <atime> 0" and wait for response - time = ScpTimestamp.parseTime(header); + time = ScpTimestampCommandDetails.parseTime(header); signalReceivedCommand(time); ScpIoUtils.writeLine(dstOut, header); @@ -203,7 +203,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { protected long transferFileData( String source, InputStream srcIn, OutputStream srcOut, String destination, InputStream dstIn, OutputStream dstOut, - ScpTimestamp time, ScpReceiveFileCommandDetails details) + ScpTimestampCommandDetails time, ScpReceiveFileCommandDetails details) throws IOException { long length = details.getLength(); if (length < 0L) { // TODO consider throwing an exception... diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java index 1322495..8ddad59 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java @@ -22,8 +22,8 @@ package org.apache.sshd.scp.client; import java.io.IOException; import org.apache.sshd.client.session.ClientSession; -import org.apache.sshd.scp.common.ScpTimestamp; import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -36,14 +36,14 @@ public interface ScpRemote2RemoteTransferListener { * @param source The source path * @param dstSession The destination {@link ClientSession} * @param destination The destination path - * @param timestamp The {@link ScpTimestamp timestamp} of the file - may be {@code null} + * @param timestamp The {@link ScpTimestampCommandDetails timestamp} of the file - may be {@code null} * @param details The {@link ScpReceiveFileCommandDetails details} of the attempted file transfer * @throws IOException If failed to handle the callback */ void startDirectFileTransfer( ClientSession srcSession, String source, ClientSession dstSession, String destination, - ScpTimestamp timestamp, ScpReceiveFileCommandDetails details) + ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details) throws IOException; /** @@ -53,7 +53,7 @@ public interface ScpRemote2RemoteTransferListener { * @param source The source path * @param dstSession The destination {@link ClientSession} * @param destination The destination path - * @param timestamp The {@link ScpTimestamp timestamp} of the file - may be {@code null} + * @param timestamp The {@link ScpTimestampCommandDetails timestamp} of the file - may be {@code null} * @param details The {@link ScpReceiveFileCommandDetails details} of the attempted file transfer * @param xferSize Number of successfully transfered bytes - zero if <tt>thrown</tt> not {@code null} * @param thrown Error thrown during transfer attempt - {@code null} if successful @@ -62,7 +62,7 @@ public interface ScpRemote2RemoteTransferListener { void endDirectFileTransfer( ClientSession srcSession, String source, ClientSession dstSession, String destination, - ScpTimestamp timestamp, ScpReceiveFileCommandDetails details, + ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details, long xferSize, Throwable thrown) throws IOException; } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java index 1967e3e..fec0e66 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java @@ -43,6 +43,7 @@ import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.SelectorUtils; import org.apache.sshd.common.util.io.DirectoryScanner; import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * Plug-in mechanism for users to intervene in the SCP process - e.g., apply some kind of traffic shaping mechanism, @@ -60,14 +61,14 @@ public interface ScpFileOpener { * @param name The target file name * @param preserve Whether requested to preserve the permissions and timestamp * @param permissions The requested file permissions - * @param time The requested {@link ScpTimestamp} - may be {@code null} if nothing to update + * @param time The requested {@link ScpTimestampCommandDetails} - may be {@code null} if nothing to update * @return The actual target file path for the incoming file/directory * @throws IOException If failed to resolve the file path - * @see #updateFileProperties(Path, Set, ScpTimestamp) updateFileProperties + * @see #updateFileProperties(Path, Set, ScpTimestampCommandDetails) updateFileProperties */ default Path resolveIncomingFilePath( Session session, Path localPath, String name, boolean preserve, Set<PosixFilePermission> permissions, - ScpTimestamp time) + ScpTimestampCommandDetails time) throws IOException { LinkOption[] options = IoUtils.getLinkOptions(true); Boolean status = IoUtils.checkFileExists(localPath, options); @@ -331,7 +332,8 @@ public interface ScpFileOpener { ScpTargetStreamResolver createScpTargetStreamResolver(Session session, Path path) throws IOException; - static void updateFileProperties(Path file, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException { + static void updateFileProperties(Path file, Set<PosixFilePermission> perms, ScpTimestampCommandDetails time) + throws IOException { IoUtils.setPermissions(file, perms); if (time != null) { diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java index 53c3f94..4a6b111 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java @@ -51,6 +51,7 @@ import org.apache.sshd.scp.common.helpers.ScpIoUtils; import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport; import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails; import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -130,7 +131,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess // https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593 public void postProcessReceivedData( String name, boolean preserve, Set<PosixFilePermission> perms, - ScpTimestamp time) + ScpTimestampCommandDetails time) throws IOException { if (log.isDebugEnabled()) { log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}", ScpHelper.this, @@ -163,7 +164,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess ScpIoUtils.receive(getSession(), in, out, log, this, handler); } - public void receiveDir(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) + public void receiveDir(String header, Path local, ScpTimestampCommandDetails time, boolean preserve, int bufferSize) throws IOException { Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); boolean debugEnabled = log.isDebugEnabled(); @@ -205,8 +206,8 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } else if (cmdChar == ScpDirEndCommandDetails.COMMAND_NAME) { ack(); break; - } else if (cmdChar == ScpTimestamp.COMMAND_NAME) { - time = ScpTimestamp.parseTime(header); + } else if (cmdChar == ScpTimestampCommandDetails.COMMAND_NAME) { + time = ScpTimestampCommandDetails.parseTime(header); ack(); } else { throw new IOException("Unexpected message: '" + header + "'"); @@ -219,7 +220,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess listener.endFolderEvent(session, FileOperation.RECEIVE, path, perms, null); } - public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) + public void receiveFile(String header, Path local, ScpTimestampCommandDetails time, boolean preserve, int bufferSize) throws IOException { Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); if (log.isDebugEnabled()) { @@ -232,7 +233,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } public void receiveStream( - String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, + String header, ScpTargetStreamResolver resolver, ScpTimestampCommandDetails time, boolean preserve, int bufferSize) throws IOException { if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) { @@ -451,7 +452,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess bufSize = MIN_SEND_BUFFER_SIZE; } - ScpTimestamp time = resolver.getTimestamp(); + ScpTimestampCommandDetails time = resolver.getTimestamp(); if (preserve && (time != null)) { int readyCode = ScpIoUtils.sendTimeCommand(in, out, time, log, this); String cmd = time.toHeader(); @@ -530,7 +531,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess BasicFileAttributes basic = opener.getLocalBasicFileAttributes(session, path, options); FileTime lastModified = basic.lastModifiedTime(); FileTime lastAccess = basic.lastAccessTime(); - ScpTimestamp time = new ScpTimestamp(lastModified, lastAccess); + ScpTimestampCommandDetails time = new ScpTimestampCommandDetails(lastModified, lastAccess); String cmd = time.toHeader(); if (debugEnabled) { log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}", this, path, lastModified, diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpReceiveLineHandler.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpReceiveLineHandler.java index 38db119..9aac7da 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpReceiveLineHandler.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpReceiveLineHandler.java @@ -22,6 +22,7 @@ package org.apache.sshd.scp.common; import java.io.IOException; import org.apache.sshd.common.session.Session; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -32,8 +33,8 @@ public interface ScpReceiveLineHandler { * @param session The client/server {@link Session} through which the transfer is being executed * @param line Received SCP input line * @param isDir Does the input line refer to a directory - * @param time The received {@link ScpTimestamp} - may be {@code null} + * @param time The received {@link ScpTimestampCommandDetails} - may be {@code null} * @throws IOException If failed to process the line */ - void process(Session session, String line, boolean isDir, ScpTimestamp time) throws IOException; + void process(Session session, String line, boolean isDir, ScpTimestampCommandDetails time) throws IOException; } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpSourceStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpSourceStreamResolver.java index 0d79d3b..3fe7d26 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpSourceStreamResolver.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpSourceStreamResolver.java @@ -28,6 +28,7 @@ import java.util.Collection; import java.util.Set; import org.apache.sshd.common.session.Session; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -51,11 +52,11 @@ public interface ScpSourceStreamResolver { Collection<PosixFilePermission> getPermissions() throws IOException; /** - * @return The {@link ScpTimestamp} to use for uploading the file if {@code null} then no need to send - * this information + * @return The {@link ScpTimestampCommandDetails} to use for uploading the file if {@code null} then no + * need to send this information * @throws IOException If failed to generate the required data */ - ScpTimestamp getTimestamp() throws IOException; + ScpTimestampCommandDetails getTimestamp() throws IOException; /** * @return An estimated size of the expected number of bytes to be uploaded. If non-positive then diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTargetStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTargetStreamResolver.java index d894224..5fd7721 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTargetStreamResolver.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTargetStreamResolver.java @@ -27,6 +27,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.util.Set; import org.apache.sshd.common.session.Session; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -83,6 +84,6 @@ public interface ScpTargetStreamResolver { * @throws IOException If failed to post-process the incoming data */ void postProcessReceivedData( - String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) + String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestampCommandDetails time) throws IOException; } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpSourceStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpSourceStreamResolver.java index 3fca826..a501ad4 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpSourceStreamResolver.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpSourceStreamResolver.java @@ -36,7 +36,6 @@ import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.scp.common.ScpFileOpener; import org.apache.sshd.scp.common.ScpSourceStreamResolver; -import org.apache.sshd.scp.common.ScpTimestamp; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -47,7 +46,7 @@ public class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implem protected final Path name; protected final Set<PosixFilePermission> perms; protected final long size; - protected final ScpTimestamp time; + protected final ScpTimestampCommandDetails time; public LocalFileScpSourceStreamResolver(Path path, ScpFileOpener opener) throws IOException { this.path = Objects.requireNonNull(path, "No path specified"); @@ -58,7 +57,7 @@ public class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implem BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class); BasicFileAttributes basic = view.readAttributes(); this.size = basic.size(); - this.time = new ScpTimestamp(basic.lastModifiedTime().toMillis(), basic.lastAccessTime().toMillis()); + this.time = new ScpTimestampCommandDetails(basic.lastModifiedTime().toMillis(), basic.lastAccessTime().toMillis()); } @Override @@ -72,7 +71,7 @@ public class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implem } @Override - public ScpTimestamp getTimestamp() throws IOException { + public ScpTimestampCommandDetails getTimestamp() throws IOException { return time; } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpTargetStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpTargetStreamResolver.java index 523c7aa..c04aa04 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpTargetStreamResolver.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/LocalFileScpTargetStreamResolver.java @@ -39,7 +39,6 @@ import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.scp.common.ScpFileOpener; import org.apache.sshd.scp.common.ScpTargetStreamResolver; -import org.apache.sshd.scp.common.ScpTimestamp; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -137,7 +136,7 @@ public class LocalFileScpTargetStreamResolver extends AbstractLoggingBean implem @Override public void postProcessReceivedData( - String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) + String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestampCommandDetails time) throws IOException { if (file == null) { throw new StreamCorruptedException( @@ -150,7 +149,7 @@ public class LocalFileScpTargetStreamResolver extends AbstractLoggingBean implem } protected void updateFileProperties( - String name, Path path, Set<PosixFilePermission> perms, ScpTimestamp time) + String name, Path path, Set<PosixFilePermission> perms, ScpTimestampCommandDetails time) throws IOException { boolean traceEnabled = log.isTraceEnabled(); if (traceEnabled) { diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java index b1a638f..3075384 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java @@ -32,8 +32,36 @@ public class ScpDirEndCommandDetails extends AbstractScpCommandDetails { super(COMMAND_NAME); } + public ScpDirEndCommandDetails(String header) { + super(COMMAND_NAME); + if (!HEADER.equals(header)) { + throw new IllegalArgumentException("Mismatched header - expected '" + HEADER + "' but got '" + header + "'"); + } + } + @Override public String toHeader() { return HEADER; } + + @Override + public int hashCode() { + return HEADER.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + + // All ScpDirEndCommandDetails are equal to each other + return true; + } } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java index b8e271c..0f95834 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java @@ -41,7 +41,6 @@ import org.apache.sshd.core.CoreModuleProperties; import org.apache.sshd.scp.ScpModuleProperties; import org.apache.sshd.scp.common.ScpException; import org.apache.sshd.scp.common.ScpReceiveLineHandler; -import org.apache.sshd.scp.common.ScpTimestamp; import org.slf4j.Logger; /** @@ -93,14 +92,14 @@ public final class ScpIoUtils { * * @param in The {@link InputStream} to read from * @param out The target {@link OutputStream} - * @param time The {@link ScpTimestamp} value to send + * @param time The {@link ScpTimestampCommandDetails} value to send * @param log An optional {@link Logger} to use for issuing log messages - ignored if {@code null} * @param logHint An optional hint to be used in the logged messages to identifier the caller's context * @return The read ACK value * @throws IOException If failed to complete the read/write cyle */ public static int sendTimeCommand( - InputStream in, OutputStream out, ScpTimestamp time, Logger log, Object logHint) + InputStream in, OutputStream out, ScpTimestampCommandDetails time, Logger log, Object logHint) throws IOException { String cmd = time.toHeader(); if ((log != null) && log.isDebugEnabled()) { @@ -201,7 +200,7 @@ public final class ScpIoUtils { ack(out); boolean debugEnabled = (log != null) && log.isDebugEnabled(); - for (ScpTimestamp time = null;;) { + for (ScpTimestampCommandDetails time = null;;) { String line; boolean isDir = false; int c = readAck(in, true, log, logHint); @@ -223,13 +222,13 @@ public final class ScpIoUtils { log.debug("receive({}) - Received 'C' header: {}", logHint, line); } break; - case ScpTimestamp.COMMAND_NAME: + case ScpTimestampCommandDetails.COMMAND_NAME: line = readLine(in); line = Character.toString((char) c) + line; if (debugEnabled) { log.debug("receive({}) - Received 'T' header: {}", logHint, line); } - time = ScpTimestamp.parseTime(line); + time = ScpTimestampCommandDetails.parseTime(line); ack(out); continue; case ScpDirEndCommandDetails.COMMAND_NAME: diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java index 5197656..b2a8dfc 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java @@ -22,9 +22,11 @@ package org.apache.sshd.scp.common.helpers; import java.nio.file.attribute.PosixFilePermission; import java.util.Collection; import java.util.EnumSet; +import java.util.Objects; import java.util.Set; import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; /** @@ -76,6 +78,10 @@ public abstract class ScpPathCommandDetailsSupport extends AbstractScpCommandDet return length; } + protected long getEffectiveLength() { + return getLength(); + } + public void setLength(long length) { this.length = length; } @@ -91,7 +97,34 @@ public abstract class ScpPathCommandDetailsSupport extends AbstractScpCommandDet @Override public String toHeader() { - return getCommand() + getOctalPermissions(getPermissions()) + " " + getLength() + " " + getName(); + return getCommand() + getOctalPermissions(getPermissions()) + " " + getEffectiveLength() + " " + getName(); + } + + @Override + public int hashCode() { + return Character.hashCode(getCommand()) + + 31 * Objects.hashCode(getName()) + + 37 * Long.hashCode(getEffectiveLength()) + + 41 * GenericUtils.size(getPermissions()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + + ScpPathCommandDetailsSupport other = (ScpPathCommandDetailsSupport) obj; + return (getCommand() == other.getCommand()) + && (getEffectiveLength() == other.getEffectiveLength()) + && Objects.equals(getName(), other.getName()) + && GenericUtils.equals(getPermissions(), other.getPermissions()); } @Override diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java index f9f67ee..efcc77f 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java @@ -23,7 +23,7 @@ import org.apache.sshd.common.util.GenericUtils; /** * Holds the details of a "Dmmmm <length> <directory>" command - e.g., "D0755 0 dirname" - * + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public class ScpReceiveDirCommandDetails extends ScpPathCommandDetailsSupport { @@ -38,6 +38,11 @@ public class ScpReceiveDirCommandDetails extends ScpPathCommandDetailsSupport { super(COMMAND_NAME, header); } + @Override // length is irrelevant for 'D' commands + protected long getEffectiveLength() { + return 0L; + } + public static ScpReceiveDirCommandDetails parse(String header) { return GenericUtils.isEmpty(header) ? null : new ScpReceiveDirCommandDetails(header); } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java similarity index 70% rename from sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java rename to sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java index 23fcb5d..e1a085d 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java @@ -17,27 +17,26 @@ * under the License. */ -package org.apache.sshd.scp.common; +package org.apache.sshd.scp.common.helpers; import java.nio.file.attribute.FileTime; import java.util.Date; import java.util.concurrent.TimeUnit; import org.apache.sshd.common.util.GenericUtils; -import org.apache.sshd.scp.common.helpers.AbstractScpCommandDetails; /** * Represents an SCP timestamp definition * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ -public class ScpTimestamp extends AbstractScpCommandDetails { +public class ScpTimestampCommandDetails extends AbstractScpCommandDetails { public static final char COMMAND_NAME = 'T'; private final long lastModifiedTime; private final long lastAccessTime; - public ScpTimestamp(String header) { + public ScpTimestampCommandDetails(String header) { super(COMMAND_NAME); if (header.charAt(0) != COMMAND_NAME) { @@ -49,11 +48,11 @@ public class ScpTimestamp extends AbstractScpCommandDetails { lastAccessTime = TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])); } - public ScpTimestamp(FileTime modTime, FileTime accTime) { + public ScpTimestampCommandDetails(FileTime modTime, FileTime accTime) { this(modTime.to(TimeUnit.MILLISECONDS), accTime.to(TimeUnit.MILLISECONDS)); } - public ScpTimestamp(long modTime, long accTime) { + public ScpTimestampCommandDetails(long modTime, long accTime) { super(COMMAND_NAME); lastModifiedTime = modTime; @@ -71,7 +70,29 @@ public class ScpTimestamp extends AbstractScpCommandDetails { @Override public String toHeader() { return Character.toString(getCommand()) + TimeUnit.MILLISECONDS.toSeconds(getLastModifiedTime()) - + " 0 " + TimeUnit.MILLISECONDS.toSeconds(getLastAccessTime()) + "0"; + + " 0 " + TimeUnit.MILLISECONDS.toSeconds(getLastAccessTime()) + " 0"; + } + + @Override + public int hashCode() { + return Long.hashCode(getLastModifiedTime()) + 31 * Long.hashCode(getLastAccessTime()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + + ScpTimestampCommandDetails other = (ScpTimestampCommandDetails) obj; + return (getLastModifiedTime() == other.getLastModifiedTime()) + && (getLastAccessTime() == other.getLastAccessTime()); } @Override @@ -84,12 +105,13 @@ public class ScpTimestamp extends AbstractScpCommandDetails { * @param line The time specification - format: * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>} where specified * times are in seconds since UTC - ignored if {@code null} - * @return The {@link ScpTimestamp} value with the timestamps converted to <U>milliseconds</U> + * @return The {@link ScpTimestampCommandDetails} value with the timestamps converted to + * <U>milliseconds</U> * @throws NumberFormatException if bad numerical values - <B>Note:</B> validates that 1st character is 'T'. * @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the * SCP protocol works</A> */ - public static ScpTimestamp parseTime(String line) throws NumberFormatException { - return GenericUtils.isEmpty(line) ? null : new ScpTimestamp(line); + public static ScpTimestampCommandDetails parseTime(String line) throws NumberFormatException { + return GenericUtils.isEmpty(line) ? null : new ScpTimestampCommandDetails(line); } } diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java index 7c0508b..f28739c 100644 --- a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java +++ b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java @@ -27,8 +27,8 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.scp.common.ScpHelper; -import org.apache.sshd.scp.common.ScpTimestamp; import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; import org.apache.sshd.util.test.CommonTestSupportUtils; import org.junit.BeforeClass; import org.junit.FixMethodOrder; @@ -76,7 +76,7 @@ public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport { public void startDirectFileTransfer( ClientSession srcSession, String source, ClientSession dstSession, String destination, - ScpTimestamp timestamp, ScpReceiveFileCommandDetails details) + ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details) throws IOException { assertEquals("Mismatched start xfer source path", srcPath, source); assertEquals("Mismatched start xfer destination path", dstPath, destination); @@ -86,7 +86,7 @@ public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport { public void endDirectFileTransfer( ClientSession srcSession, String source, ClientSession dstSession, String destination, - ScpTimestamp timestamp, ScpReceiveFileCommandDetails details, + ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details, long xferSize, Throwable thrown) throws IOException { assertEquals("Mismatched end xfer source path", srcPath, source); diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/common/helpers/AbstractScpCommandDetailsTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/common/helpers/AbstractScpCommandDetailsTest.java new file mode 100644 index 0000000..8bfcfc7 --- /dev/null +++ b/sshd-scp/src/test/java/org/apache/sshd/scp/common/helpers/AbstractScpCommandDetailsTest.java @@ -0,0 +1,88 @@ +/* + * 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.scp.common.helpers; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized.UseParametersRunnerFactory; + +/** + * @param <C> Generic {@link AbstractScpCommandDetails} type + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests +@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) +public class AbstractScpCommandDetailsTest<C extends AbstractScpCommandDetails> extends JUnitTestSupport { + private final String header; + private final Constructor<C> ctor; + + public AbstractScpCommandDetailsTest(String header, Class<C> cmdClass) throws Exception { + this.header = header; + this.ctor = cmdClass.getDeclaredConstructor(String.class); + } + + @Parameters(name = "cmd={0}") + public static List<Object[]> parameters() { + return new ArrayList<Object[]>() { + // not serializing it + private static final long serialVersionUID = 1L; + + { + addTestCase("T123456789 0 987654321 0", ScpTimestampCommandDetails.class); + addTestCase("C0644 12345 file", ScpReceiveFileCommandDetails.class); + addTestCase("D0755 0 dir", ScpReceiveDirCommandDetails.class); + addTestCase(ScpDirEndCommandDetails.HEADER, ScpDirEndCommandDetails.class); + } + + private void addTestCase(String header, Class<? extends AbstractScpCommandDetails> cmdClass) { + add(new Object[] { header, cmdClass }); + } + }; + } + + @Test + public void testHeaderEquality() throws Exception { + C details = ctor.newInstance(header); + assertEquals(header, details.toHeader()); + } + + @Test + public void testDetailsEquality() throws Exception { + C d1 = ctor.newInstance(header); + C d2 = ctor.newInstance(header); + assertEquals("HASH ?", d1.hashCode(), d2.hashCode()); + assertEquals("EQ ?", d1, d2); + } +} diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/server/ScpReceiveDirCommandDetailsTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/server/ScpReceiveDirCommandDetailsTest.java new file mode 100644 index 0000000..78bccf3 --- /dev/null +++ b/sshd-scp/src/test/java/org/apache/sshd/scp/server/ScpReceiveDirCommandDetailsTest.java @@ -0,0 +1,49 @@ +/* + * 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.scp.server; + +import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class ScpReceiveDirCommandDetailsTest extends JUnitTestSupport { + public ScpReceiveDirCommandDetailsTest() { + super(); + } + + @Test + public void testLengthDoesNotInfluenceEquality() { + ScpReceiveDirCommandDetails d1 = new ScpReceiveDirCommandDetails("D0555 0 " + getCurrentTestName()); + ScpReceiveDirCommandDetails d2 = new ScpReceiveDirCommandDetails(d1.toHeader()); + d2.setLength(d1.getLength() + 1234L); + assertNotEquals("Len ?", d1.getLength(), d2.getLength()); + assertEquals("Hash ?", d1.hashCode(), d2.hashCode()); + assertEquals("EQ", d1, d2); + } +}