This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git


The following commit(s) were added to refs/heads/master by this push:
     new 5174788  [SSHD-1009] SSHD SCP does not work with WinSCP
5174788 is described below

commit 51747885f9f4fa5ccffb4f29e573c6c0f363eadc
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Wed Jun 3 21:58:57 2020 +0200

    [SSHD-1009] SSHD SCP does not work with WinSCP
---
 .../apache/sshd/server/scp/InputStreamReader.java  | 346 ++++++++++++
 .../apache/sshd/server/scp/ScpCommandFactory.java  |  19 +-
 .../java/org/apache/sshd/server/scp/ScpShell.java  | 588 +++++++++++++++++++++
 .../java/org/apache/sshd/client/scp/ScpTest.java   |   4 +-
 4 files changed, 953 insertions(+), 4 deletions(-)

diff --git 
a/sshd-scp/src/main/java/org/apache/sshd/server/scp/InputStreamReader.java 
b/sshd-scp/src/main/java/org/apache/sshd/server/scp/InputStreamReader.java
new file mode 100644
index 0000000..cff78e9
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/InputStreamReader.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2002-2016, the original author or authors.
+ *
+ * This software is distributable under the BSD license. See the terms of the
+ * BSD license in the documentation provided with this software.
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+package org.apache.sshd.server.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
+import java.nio.charset.UnmappableCharacterException;
+
+
+/**
+ *
+ * NOTE for SSHD: the default InputStreamReader that comes from the JRE
+ * usually read more bytes than needed from the input stream, which
+ * is not usable in a character per character model used in the terminal.
+ * We thus use the harmony code which only reads the minimal number of bytes.
+ */
+
+/**
+ * A class for turning a byte stream into a character stream. Data read from 
the
+ * source input stream is converted into characters by either a default or a
+ * provided character converter. The default encoding is taken from the
+ * "file.encoding" system property. {@code InputStreamReader} contains a buffer
+ * of bytes read from the source stream and converts these into characters as
+ * needed. The buffer size is 8K.
+ * 
+ * @see OutputStreamWriter
+ */
+public class InputStreamReader extends Reader {
+    private InputStream in;
+
+    private static final int BUFFER_SIZE = 4;
+
+    private boolean endOfInput = false;
+
+    CharsetDecoder decoder;
+
+    ByteBuffer bytes = ByteBuffer.allocate(BUFFER_SIZE);
+
+    char pending = (char) -1;
+
+    /**
+     * Constructs a new {@code InputStreamReader} on the {@link InputStream}
+     * {@code in}. This constructor sets the character converter to the 
encoding
+     * specified in the "file.encoding" property and falls back to ISO 8859_1
+     * (ISO-Latin-1) if the property doesn't exist.
+     * 
+     * @param in
+     *            the input stream from which to read characters.
+     */
+    public InputStreamReader(InputStream in) {
+        super(in);
+        this.in = in;
+        decoder = Charset.defaultCharset().newDecoder().onMalformedInput(
+                CodingErrorAction.REPLACE).onUnmappableCharacter(
+                CodingErrorAction.REPLACE);
+        bytes.limit(0);
+    }
+
+    /**
+     * Constructs a new InputStreamReader on the InputStream {@code in}. The
+     * character converter that is used to decode bytes into characters is
+     * identified by name by {@code enc}. If the encoding cannot be found, an
+     * UnsupportedEncodingException error is thrown.
+     * 
+     * @param in
+     *            the InputStream from which to read characters.
+     * @param enc
+     *            identifies the character converter to use.
+     * @throws NullPointerException
+     *             if {@code enc} is {@code null}.
+     * @throws UnsupportedEncodingException
+     *             if the encoding specified by {@code enc} cannot be found.
+     */
+    public InputStreamReader(InputStream in, final String enc)
+            throws UnsupportedEncodingException {
+        super(in);
+        if (enc == null) {
+            throw new NullPointerException();
+        }
+        this.in = in;
+        try {
+            decoder = Charset.forName(enc).newDecoder().onMalformedInput(
+                    CodingErrorAction.REPLACE).onUnmappableCharacter(
+                    CodingErrorAction.REPLACE);
+        } catch (IllegalArgumentException e) {
+            throw (UnsupportedEncodingException)
+                    new UnsupportedEncodingException(enc).initCause(e);
+        }
+        bytes.limit(0);
+    }
+
+    /**
+     * Constructs a new InputStreamReader on the InputStream {@code in} and
+     * CharsetDecoder {@code dec}.
+     * 
+     * @param in
+     *            the source InputStream from which to read characters.
+     * @param dec
+     *            the CharsetDecoder used by the character conversion.
+     */
+    public InputStreamReader(InputStream in, CharsetDecoder dec) {
+        super(in);
+        dec.averageCharsPerByte();
+        this.in = in;
+        decoder = dec;
+        bytes.limit(0);
+    }
+
+    /**
+     * Constructs a new InputStreamReader on the InputStream {@code in} and
+     * Charset {@code charset}.
+     * 
+     * @param in
+     *            the source InputStream from which to read characters.
+     * @param charset
+     *            the Charset that defines the character converter
+     */
+    public InputStreamReader(InputStream in, Charset charset) {
+        super(in);
+        this.in = in;
+        decoder = charset.newDecoder().onMalformedInput(
+                CodingErrorAction.REPLACE).onUnmappableCharacter(
+                CodingErrorAction.REPLACE);
+        bytes.limit(0);
+    }
+
+    /**
+     * Closes this reader. This implementation closes the source InputStream 
and
+     * releases all local storage.
+     * 
+     * @throws IOException
+     *             if an error occurs attempting to close this reader.
+     */
+    @Override
+    public void close() throws IOException {
+        synchronized (lock) {
+            decoder = null;
+            if (in != null) {
+                in.close();
+                in = null;
+            }
+        }
+    }
+
+    /**
+     * Returns the name of the encoding used to convert bytes into characters.
+     * The value {@code null} is returned if this reader has been closed.
+     * 
+     * @return the name of the character converter or {@code null} if this
+     *         reader is closed.
+     */
+    public String getEncoding() {
+        if (!isOpen()) {
+            return null;
+        }
+        return decoder.charset().name();
+    }
+
+    /**
+     * Reads a single character from this reader and returns it as an integer
+     * with the two higher-order bytes set to 0. Returns -1 if the end of the
+     * reader has been reached. The byte value is either obtained from
+     * converting bytes in this reader's buffer or by first filling the buffer
+     * from the source InputStream and then reading from the buffer.
+     * 
+     * @return the character read or -1 if the end of the reader has been
+     *         reached.
+     * @throws IOException
+     *             if this reader is closed or some other I/O error occurs.
+     */
+    @Override
+    public int read() throws IOException {
+        synchronized (lock) {
+            if (!isOpen()) {
+                throw new IOException("InputStreamReader is closed.");
+            }
+
+            if (pending != (char) -1) {
+                char c = pending;
+                pending = (char) -1;
+                return c;
+            }
+            char buf[] = new char[2];
+            int nb = read(buf, 0, 2);
+            if (nb == 2) {
+                pending = buf[1];
+            }
+            if (nb > 0) {
+                return buf[0];
+            } else {
+                return -1;
+            }
+        }
+    }
+
+    /**
+     * Reads at most {@code length} characters from this reader and stores them
+     * at position {@code offset} in the character array {@code buf}. Returns
+     * the number of characters actually read or -1 if the end of the reader 
has
+     * been reached. The bytes are either obtained from converting bytes in 
this
+     * reader's buffer or by first filling the buffer from the source
+     * InputStream and then reading from the buffer.
+     * 
+     * @param buf
+     *            the array to store the characters read.
+     * @param offset
+     *            the initial position in {@code buf} to store the characters
+     *            read from this reader.
+     * @param length
+     *            the maximum number of characters to read.
+     * @return the number of characters read or -1 if the end of the reader has
+     *         been reached.
+     * @throws IndexOutOfBoundsException
+     *             if {@code offset < 0} or {@code length < 0}, or if
+     *             {@code offset + length} is greater than the length of
+     *             {@code buf}.
+     * @throws IOException
+     *             if this reader is closed or some other I/O error occurs.
+     */
+    @Override
+    public int read(char[] buf, int offset, int length) throws IOException {
+        synchronized (lock) {
+            if (!isOpen()) {
+                throw new IOException("InputStreamReader is closed.");
+            }
+            if (offset < 0 || offset > buf.length - length || length < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+            if (length == 0) {
+                return 0;
+            }
+
+            CharBuffer out = CharBuffer.wrap(buf, offset, length);
+            CoderResult result = CoderResult.UNDERFLOW;
+
+            // bytes.remaining() indicates number of bytes in buffer
+            // when 1-st time entered, it'll be equal to zero
+            boolean needInput = !bytes.hasRemaining();
+
+            while (out.position() == offset) {
+                // fill the buffer if needed
+                if (needInput) {
+                    try {
+                        if ((in.available() == 0) 
+                            && (out.position() > offset)) {
+                            // we could return the result without blocking read
+                            break;
+                        }
+                    } catch (IOException e) {
+                        // available didn't work so just try the read
+                    }
+
+                    int off = bytes.arrayOffset() + bytes.limit();
+                    int was_red = in.read(bytes.array(), off, 1);
+
+                    if (was_red == -1) {
+                        endOfInput = true;
+                        break;
+                    } else if (was_red == 0) {
+                        break;
+                    }
+                    bytes.limit(bytes.limit() + was_red);
+                }
+
+                // decode bytes
+                result = decoder.decode(bytes, out, false);
+
+                if (result.isUnderflow()) {
+                    // compact the buffer if no space left
+                    if (bytes.limit() == bytes.capacity()) {
+                        bytes.compact();
+                        bytes.limit(bytes.position());
+                        bytes.position(0);
+                    }
+                    needInput = true;
+                } else {
+                    break;
+                }
+            }
+
+            if (result == CoderResult.UNDERFLOW && endOfInput) {
+                result = decoder.decode(bytes, out, true);
+                decoder.flush(out);
+                decoder.reset();
+            }
+            if (result.isMalformed()) {
+                throw new MalformedInputException(result.length());
+            } else if (result.isUnmappable()) {
+                throw new UnmappableCharacterException(result.length());
+            }
+
+            return out.position() - offset == 0 ? -1 : out.position() - offset;
+        }
+    }
+
+    /*
+     * Answer a boolean indicating whether or not this InputStreamReader is
+     * open.
+     */
+    private boolean isOpen() {
+        return in != null;
+    }
+
+    /**
+     * Indicates whether this reader is ready to be read without blocking. If
+     * the result is {@code true}, the next {@code read()} will not block. If
+     * the result is {@code false} then this reader may or may not block when
+     * {@code read()} is called. This implementation returns {@code true} if
+     * there are bytes available in the buffer or the source stream has bytes
+     * available.
+     * 
+     * @return {@code true} if the receiver will not block when {@code read()}
+     *         is called, {@code false} if unknown or blocking will occur.
+     * @throws IOException
+     *             if this reader is closed or some other I/O error occurs.
+     */
+    @Override
+    public boolean ready() throws IOException {
+        synchronized (lock) {
+            if (in == null) {
+                throw new IOException("InputStreamReader is closed.");
+            }
+            try {
+                return bytes.hasRemaining() || in.available() > 0;
+            } catch (IOException e) {
+                return false;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java 
b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
index 0c74489..8dafe9e 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.server.scp;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.function.Supplier;
@@ -31,9 +32,11 @@ import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ObjectBuilder;
 import org.apache.sshd.common.util.threads.CloseableExecutorService;
 import org.apache.sshd.common.util.threads.ManagedExecutorServiceSupplier;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.AbstractDelegatingCommandFactory;
 import org.apache.sshd.server.command.Command;
 import org.apache.sshd.server.command.CommandFactory;
+import org.apache.sshd.server.shell.ShellFactory;
 
 /**
  * This <code>CommandFactory</code> can be used as a standalone command 
factory or can be used to augment another
@@ -41,10 +44,11 @@ import org.apache.sshd.server.command.CommandFactory;
  *
  * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
  * @see    ScpCommand
+ * @see    ScpShell
  */
 public class ScpCommandFactory
         extends AbstractDelegatingCommandFactory
-        implements ManagedExecutorServiceSupplier, ScpFileOpenerHolder, 
Cloneable {
+        implements ManagedExecutorServiceSupplier, ScpFileOpenerHolder, 
Cloneable, ShellFactory {
 
     public static final String SCP_FACTORY_NAME = "scp";
 
@@ -102,8 +106,8 @@ public class ScpCommandFactory
 
     private Supplier<? extends CloseableExecutorService> executorsProvider;
     private ScpFileOpener fileOpener;
-    private int sendBufferSize = ScpHelper.MIN_SEND_BUFFER_SIZE;
-    private int receiveBufferSize = ScpHelper.MIN_RECEIVE_BUFFER_SIZE;
+    private int sendBufferSize = ScpHelper.DEFAULT_SEND_BUFFER_SIZE;
+    private int receiveBufferSize = ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE;
     private Collection<ScpTransferEventListener> listeners = new 
CopyOnWriteArraySet<>();
     private ScpTransferEventListener listenerProxy;
 
@@ -215,6 +219,15 @@ public class ScpCommandFactory
                 getScpFileOpener(), listenerProxy);
     }
 
+    @Override
+    public Command createShell(ChannelSession channel) throws IOException {
+        return new ScpShell(
+                channel,
+                resolveExecutorService(),
+                getSendBufferSize(), getReceiveBufferSize(),
+                getScpFileOpener(), listenerProxy);
+    }
+
     protected CloseableExecutorService resolveExecutorService(String command) {
         return resolveExecutorService();
     }
diff --git a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpShell.java 
b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpShell.java
new file mode 100644
index 0000000..adbebb1
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpShell.java
@@ -0,0 +1,588 @@
+/*
+ * 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.server.scp;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.threads.CloseableExecutorService;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.AbstractFileSystemCommand;
+
+/**
+ * This commands SCP support for a ChannelSession.
+ *
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ */
+public class ScpShell extends AbstractFileSystemCommand {
+
+    public static final String STATUS = "status";
+
+    protected static final boolean IS_WINDOWS = 
System.getProperty("os.name").toLowerCase().contains("win");
+
+    protected static final List<String> WINDOWS_EXECUTABLE_EXTENSIONS = 
Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd"));
+    protected static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0];
+
+
+    protected final ChannelSession channel;
+    protected ScpFileOpener opener;
+    protected ScpTransferEventListener listener;
+    protected int sendBufferSize;
+    protected int receiveBufferSize;
+    protected Path currentDir;
+    protected Map<String, Object> variables = new HashMap<>();
+
+    public ScpShell(ChannelSession channel, CloseableExecutorService 
executorService,
+                    int sendSize, int receiveSize,
+                    ScpFileOpener fileOpener, ScpTransferEventListener 
eventListener) {
+        super(null, executorService);
+        this.channel = channel;
+
+        if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
+            throw new IllegalArgumentException(
+                    "<ScpShell> send buffer size "
+                            + "(" + sendSize + ") below minimum required "
+                            + "(" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
+        }
+        sendBufferSize = sendSize;
+
+        if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
+            throw new IllegalArgumentException(
+                    "<ScpCommmand> receive buffer size "
+                            + "(" + sendSize + ") below minimum required "
+                            + "(" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
+        }
+        receiveBufferSize = receiveSize;
+
+        opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : 
fileOpener;
+        listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : 
eventListener;
+
+    }
+
+    protected void println(Object x, OutputStream out) {
+        try {
+            String s = x + System.lineSeparator();
+            out.write(s.getBytes());
+        } catch (IOException e) {
+            throw new IOError(e);
+        }
+    }
+
+    @Override
+    public void run() {
+        String command = null;
+        try {
+            currentDir = opener.resolveLocalPath(channel.getSession(), 
fileSystem, ".");
+            // Use a special stream reader so that the stream can be used with 
the scp command
+            try (Reader r = new InputStreamReader(getInputStream(), 
StandardCharsets.UTF_8)) {
+                for (;;) {
+                    command = readLine(r);
+                    if (command.length() == 0 || !handleCommandLine(command)) {
+                        return;
+                    }
+                }
+            }
+        } catch (InterruptedIOException e) {
+            // Ignore - signaled end
+        } catch (Exception e) {
+            String message = "Failed (" + e.getClass().getSimpleName() + ") to 
handle '" + command + "': " + e.getMessage();
+            try {
+                OutputStream stderr = getErrorStream();
+                stderr.write(message.getBytes(StandardCharsets.US_ASCII));
+            } catch (IOException ioe) {
+                log.warn("Failed ({}) to write error message={}: {}",
+                        e.getClass().getSimpleName(), message, 
ioe.getMessage());
+            } finally {
+                onExit(-1, message);
+            }
+        } finally {
+            onExit(0);
+        }
+    }
+
+    protected String readLine(Reader reader) throws IOException {
+        StringBuilder sb = new StringBuilder();
+        int c;
+        while ((c = reader.read()) >= 0) {
+            if (c == '\n') {
+                break;
+            }
+            sb.append((char) c);
+        }
+        return sb.toString();
+    }
+
+    protected boolean handleCommandLine(String command) throws Exception {
+        List<String[]> cmds = parse(command);
+        for (String[] argv : cmds) {
+            switch (argv[0]) {
+                case "echo":
+                    echo(argv);
+                    break;
+                case "pwd":
+                    pwd(argv);
+                    break;
+                case "cd":
+                    cd(argv);
+                    break;
+                case "ls":
+                    ls(argv);
+                    break;
+                case "scp":
+                    scp(argv);
+                    break;
+                case "groups":
+                    variables.put(STATUS, 0);
+                    break;
+                case "unset":
+                case "unalias":
+                case "printenv":
+                    variables.put(STATUS, 1);
+                    break;
+                default:
+                    variables.put(STATUS, 127);
+                    getErrorStream().write(("command not found: " + argv[0] + 
"\n").getBytes());
+            }
+            getOutputStream().flush();
+            getErrorStream().flush();
+        }
+        return true;
+    }
+
+    protected List<String[]> parse(String command) {
+        List<String[]> cmds = new ArrayList<>();
+        List<String> args = new ArrayList<>();
+        StringBuilder arg = new StringBuilder();
+        char quote = 0;
+        boolean escaped = false;
+        for (int i = 0; i < command.length(); i++) {
+            char ch = command.charAt(i);
+            if (escaped) {
+                arg.append(ch);
+                escaped = false;
+            } else if (ch == quote) {
+                quote = 0;
+            } else if (ch == '"' || ch == '\'') {
+                quote = ch;
+            } else if (ch == '\\') {
+                escaped = true;
+            } else if (quote == 0 && Character.isWhitespace(ch)) {
+                if (arg.length() > 0) {
+                    args.add(arg.toString());
+                    arg.setLength(0);
+                }
+            } else if (quote == 0 && ch == ';') {
+                if (arg.length() > 0) {
+                    args.add(arg.toString());
+                    arg.setLength(0);
+                }
+                if (!args.isEmpty()) {
+                    cmds.add(args.toArray(new String[0]));
+                }
+                args.clear();
+            } else {
+                arg.append(ch);
+            }
+        }
+        if (arg.length() > 0) {
+            args.add(arg.toString());
+            arg.setLength(0);
+        }
+        if (!args.isEmpty()) {
+            cmds.add(args.toArray(new String[0]));
+        }
+        return cmds;
+    }
+
+    protected void scp(String[] argv) throws Exception {
+        boolean optR = false;
+        boolean optT = false;
+        boolean optF = false;
+        boolean optD = false;
+        boolean optP = false;
+        boolean isOption = true;
+        String path = null;
+        for (int i = 1; i < argv.length; i++) {
+            if (isOption && argv[i].startsWith("-")) {
+                switch (argv[i]) {
+                    case "-r": optR = true; break;
+                    case "-t": optT = true; break;
+                    case "-f": optF = true; break;
+                    case "-d": optD = true; break;
+                    case "-p": optP = true; break;
+                    default:
+                        println("scp: unsupported option: " + argv[i], 
getErrorStream());
+                        variables.put(STATUS, 1);
+                        return;
+                }
+            } else if (path == null) {
+                path = argv[i];
+                isOption = false;
+            } else {
+                println("scp: one and only one argument expected", 
getErrorStream());
+                variables.put(STATUS, 1);
+                return;
+            }
+        }
+        if (optT && optF || !optT && !optF) {
+            println("scp: one and only one of -t and -f option expected", 
getErrorStream());
+            variables.put(STATUS, 1);
+        } else {
+            try {
+                ScpHelper helper = new ScpHelper(channel.getSession(), 
getInputStream(), getOutputStream(),
+                                                 fileSystem, opener, listener);
+                if (optT) {
+                    helper.receive(helper.resolveLocalPath(path), optR, optD, 
optP, receiveBufferSize);
+                } else {
+                    helper.send(Collections.singletonList(path), optR, optP, 
sendBufferSize);
+                }
+                variables.put(STATUS, 0);
+            } catch (IOException e) {
+                Integer statusCode = e instanceof ScpException ? 
((ScpException) e).getExitStatus() : null;
+                int exitValue = (statusCode == null) ? ScpHelper.ERROR : 
statusCode;
+                // this is an exception so status cannot be OK/WARNING
+                if ((exitValue == ScpHelper.OK) || (exitValue == 
ScpHelper.WARNING)) {
+                    exitValue = ScpHelper.ERROR;
+                }
+                String exitMessage = GenericUtils.trimToEmpty(e.getMessage());
+                ScpHelper.sendResponseMessage(getOutputStream(), exitValue, 
exitMessage);
+                variables.put(STATUS, exitValue);
+            }
+        }
+    }
+
+
+    protected void echo(String[] argv) throws Exception {
+        StringBuilder buf = new StringBuilder();
+        for (int k = 1; k < argv.length; k++) {
+            String arg = argv[k];
+            if (buf.length() > 0) {
+                buf.append(' ');
+            }
+            int vstart = -1;
+            for (int i = 0; i < arg.length(); i++) {
+                int c = arg.charAt(i);
+                if (vstart >= 0) {
+                    if (c != '_' && (c < '0' || c > '9') && (c < 'A' || c > 
'Z') && (c < 'a' || c > 'z')) {
+                        if (vstart == i) {
+                            buf.append('$');
+                        } else {
+                            String n = arg.substring(vstart, i);
+                            Object v = variables.get(n);
+                            if (v != null) {
+                                buf.append(v);
+                            }
+                        }
+                        vstart = -1;
+                    }
+                } else if (c == '$') {
+                    vstart = i + 1;
+               } else {
+                    buf.append((char) c);
+                }
+            }
+            if (vstart >= 0) {
+                String n = arg.substring(vstart);
+                if (n.isEmpty()) {
+                    buf.append('$');
+                } else {
+                    Object v = variables.get(n);
+                    if (v != null) {
+                        buf.append(v);
+                    }
+                }
+            }
+        }
+        println(buf, getOutputStream());
+        variables.put(STATUS, 0);
+    }
+
+    protected void pwd(String[] argv) throws Exception {
+        if (argv.length != 1) {
+            println("pwd: too many arguments", getErrorStream());
+            variables.put(STATUS, 1);
+        } else {
+            println(currentDir, getOutputStream());
+            variables.put(STATUS, 0);
+        }
+    }
+
+    protected void cd(String[] argv) throws Exception {
+        if (argv.length != 2) {
+            println("cd: too many or too few arguments", getErrorStream());
+            variables.put(STATUS, 1);
+        } else {
+            Path cwd = currentDir;
+            String path = argv[1];
+            cwd = cwd.resolve(path).toAbsolutePath().normalize();
+            if (!Files.exists(cwd)) {
+                println("no such file or directory: " + path, 
getErrorStream());
+                variables.put(STATUS, 1);
+            } else if (!Files.isDirectory(cwd)) {
+                println("not a directory: " + path, getErrorStream());
+                variables.put(STATUS, 1);
+            } else {
+                currentDir = cwd;
+                variables.put(STATUS, 0);
+            }
+        }
+    }
+
+    protected void ls(String[] argv) throws Exception {
+        // find options
+        boolean a = false, l = false, f = false;
+        for (int k = 1; k < argv.length; k++) {
+            if (argv[k].equals("--full-time")) {
+                f = true;
+            } else if (argv[k].startsWith("-")) {
+                for (int i = 1; i < argv[k].length(); i++) {
+                    switch (argv[k].charAt(i)) {
+                        case 'a': a = true; break;
+                        case 'l': l = true; break;
+                        default:
+                            println("unsupported option: -" + 
argv[k].charAt(i), getErrorStream());
+                            variables.put(STATUS, 1);
+                            return;
+                    }
+                }
+            } else {
+                println("unsupported option: " + argv[k], getErrorStream());
+                variables.put(STATUS, 1);
+                return;
+            }
+        }
+        boolean optListAll = a;
+        boolean optLong  = l;
+        boolean optFullTime = f;
+        // list current directory content
+        Predicate<Path> filter = p -> optListAll || 
p.getFileName().toString().equals(".")
+                || p.getFileName().toString().equals("..") || 
!p.getFileName().toString().startsWith(".");
+        String[] synth = currentDir.toString().equals("/") ?  new String[] { 
"." } : new String[] { ".", ".." };
+        Stream.concat(Stream.of(synth).map(currentDir::resolve), 
Files.list(currentDir))
+                .filter(filter)
+                .map(p -> new PathEntry(p, currentDir))
+                .sorted()
+                .map(p -> p.display(optLong, optFullTime))
+                .forEach(str -> println(str, getOutputStream()));
+        variables.put(STATUS, 0);
+    }
+
+    protected static class PathEntry implements Comparable<PathEntry> {
+
+        protected final Path abs;
+        protected final Path path;
+        protected final Map<String, Object> attributes;
+
+        public PathEntry(Path abs, Path root) {
+            this.abs = abs;
+            this.path = abs.startsWith(root) ? root.relativize(abs) : abs;
+            this.attributes = readAttributes(abs);
+        }
+
+        @Override
+        public int compareTo(PathEntry o) {
+            return path.toString().compareTo(o.path.toString());
+        }
+
+        public String display(boolean optLongDisplay, boolean optFullTime) {
+            if (optLongDisplay) {
+                String username;
+                if (attributes.containsKey("owner")) {
+                    username = Objects.toString(attributes.get("owner"), null);
+                } else {
+                    username = "owner";
+                }
+                if (username.length() > 8) {
+                    username = username.substring(0, 8);
+                } else {
+                    for (int i = username.length(); i < 8; i++) {
+                        username += " ";
+                    }
+                }
+                String group;
+                if (attributes.containsKey("group")) {
+                    group = Objects.toString(attributes.get("group"), null);
+                } else {
+                    group = "group";
+                }
+                if (group.length() > 8) {
+                    group = group.substring(0, 8);
+                } else {
+                    for (int i = group.length(); i < 8; i++) {
+                        group += " ";
+                    }
+                }
+                Number length = (Number) attributes.get("size");
+                if (length == null) {
+                    length = 0L;
+                }
+                String lengthString = String.format("%1$8s", length);
+                @SuppressWarnings("unchecked")
+                Set<PosixFilePermission> perms = (Set<PosixFilePermission>) 
attributes.get("permissions");
+                if (perms == null) {
+                    perms = EnumSet.noneOf(PosixFilePermission.class);
+                }
+                // TODO: all fields should be padded to align
+                return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" 
: (is("isOther") ? "o" : "-")))
+                        + PosixFilePermissions.toString(perms) + " "
+                        + String.format("%3s", 
(attributes.containsKey("nlink") ? attributes.get("nlink").toString() : "1"))
+                        + " " + username + " " + group + " " + lengthString + 
" "
+                        + toString((FileTime) 
attributes.get("lastModifiedTime"), optFullTime)
+                        + " " + shortDisplay();
+            } else {
+                return shortDisplay();
+            }
+        }
+
+        protected boolean is(String attr) {
+            Object d = attributes.get(attr);
+            return d instanceof Boolean && (Boolean) d;
+        }
+
+        protected String shortDisplay() {
+            if (is("isSymbolicLink")) {
+                try {
+                    Path l = Files.readSymbolicLink(abs);
+                    return path.toString() + " -> " + l.toString();
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+            return path.toString();
+        }
+
+        protected String toString(FileTime time, boolean optFullTime) {
+            long millis = (time != null) ? time.toMillis() : -1L;
+            if (millis < 0L) {
+                return "------------";
+            }
+            ZonedDateTime dt = 
Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
+            if (optFullTime) {
+                return DateTimeFormatter.ofPattern("MMM ppd HH:mm:ss 
yyyy").format(dt);
+            }
+            // Less than six months
+            else if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 
60L * 1000L) {
+                return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt);
+            }
+            // Older than six months
+            else {
+                return DateTimeFormatter.ofPattern("MMM ppd  yyyy").format(dt);
+            }
+        }
+
+        protected static Map<String, Object> readAttributes(Path path) {
+            Map<String, Object>  attrs = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+            for (String view : 
path.getFileSystem().supportedFileAttributeViews()) {
+                try {
+                    Map<String, Object> ta = Files.readAttributes(path, view + 
":*", EMPTY_LINK_OPTIONS);
+                    ta.forEach(attrs::putIfAbsent);
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+            attrs.computeIfAbsent("isExecutable", s -> 
Files.isExecutable(path));
+            attrs.computeIfAbsent("permissions", s -> 
getPermissionsFromFile(path.toFile()));
+            return attrs;
+        }
+    }
+
+    /**
+     * @param f The {@link File} to be checked
+     * @return A {@link Set} of {@link PosixFilePermission}s based on whether
+     * the file is readable/writable/executable. If so, then <U>all</U> the
+     * relevant permissions are set (i.e., owner, group and others)
+     */
+    protected static Set<PosixFilePermission> getPermissionsFromFile(File f) {
+        Set<PosixFilePermission> perms = 
EnumSet.noneOf(PosixFilePermission.class);
+        if (f.canRead()) {
+            perms.add(PosixFilePermission.OWNER_READ);
+            perms.add(PosixFilePermission.GROUP_READ);
+            perms.add(PosixFilePermission.OTHERS_READ);
+        }
+
+        if (f.canWrite()) {
+            perms.add(PosixFilePermission.OWNER_WRITE);
+            perms.add(PosixFilePermission.GROUP_WRITE);
+            perms.add(PosixFilePermission.OTHERS_WRITE);
+        }
+
+        if (f.canExecute() || (IS_WINDOWS && 
isWindowsExecutable(f.getName()))) {
+            perms.add(PosixFilePermission.OWNER_EXECUTE);
+            perms.add(PosixFilePermission.GROUP_EXECUTE);
+            perms.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+
+        return perms;
+    }
+
+    /**
+     * @param fileName The file name to be evaluated - ignored if {@code 
null}/empty
+     * @return {@code true} if the file ends in one of the {@link 
#WINDOWS_EXECUTABLE_EXTENSIONS}
+     */
+    protected static boolean isWindowsExecutable(String fileName) {
+        if ((fileName == null) || (fileName.length() <= 0)) {
+            return false;
+        }
+        for (String suffix : WINDOWS_EXECUTABLE_EXTENSIONS) {
+            if (fileName.endsWith(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java 
b/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
index c32f7e7..c9ef06b 100644
--- a/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
+++ b/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
@@ -149,7 +149,9 @@ public class ScpTest extends BaseTestSupport {
     public static void setupClientAndServer() throws Exception {
         JSchLogger.init();
         sshd = CoreTestSupportUtils.setupTestServer(ScpTest.class);
-        sshd.setCommandFactory(new ScpCommandFactory());
+        ScpCommandFactory factory = new ScpCommandFactory();
+        sshd.setCommandFactory(factory);
+        sshd.setShellFactory(factory);
         sshd.start();
         port = sshd.getPort();
 

Reply via email to