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

commit f8649545fd5a1e3e4a0e4efcc1d0aa6fcc4acc52
Author: Lyor Goldstein <lgoldst...@apache.org>
AuthorDate: Fri Jun 5 11:54:03 2020 +0300

    [SSHD-1009] Fixed bugs in ScpShell to make it work on Windows
---
 .../apache/sshd/common/file/FileSystemAware.java   |  18 +
 .../apache/sshd/common/file/FileSystemFactory.java |  15 +-
 .../file/nativefs/NativeFileSystemFactory.java     |  30 +-
 .../file/virtualfs/VirtualFileSystemFactory.java   |  24 +-
 .../apache/sshd/server/channel/ChannelSession.java |   2 +-
 .../java/org/apache/sshd/server/scp/ScpShell.java  | 614 ++++++++++++++-------
 .../sshd/client/subsystem/sftp/SftpTest.java       |  14 +-
 7 files changed, 491 insertions(+), 226 deletions(-)

diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemAware.java 
b/sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemAware.java
index 5393cde..65817af 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemAware.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemAware.java
@@ -18,8 +18,11 @@
  */
 package org.apache.sshd.common.file;
 
+import java.io.IOException;
 import java.nio.file.FileSystem;
 
+import org.apache.sshd.common.session.SessionContext;
+
 /**
  * Interface that can be implemented by a command to be able to access the 
file system in which this command will be
  * used.
@@ -27,6 +30,21 @@ import java.nio.file.FileSystem;
 @FunctionalInterface
 public interface FileSystemAware {
     /**
+     * Sets the {@link FileSystemFactory} used to create the {@link 
FileSystem} to be used by the session
+     *
+     * @param  factory     The factory instance
+     * @param  session     The {@link SessionContext}
+     * @throws IOException If failed to resolve/create the file system
+     * @see                #setFileSystem(FileSystem)
+     */
+    default void setFileSystemFactory(
+            FileSystemFactory factory, SessionContext session)
+            throws IOException {
+        FileSystem fs = factory.createFileSystem(session);
+        setFileSystem(fs);
+    }
+
+    /**
      * Set the file system in which this shell will be executed.
      *
      * @param fileSystem the file system
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/file/FileSystemFactory.java 
b/sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemFactory.java
similarity index 71%
rename from 
sshd-core/src/main/java/org/apache/sshd/common/file/FileSystemFactory.java
rename to 
sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemFactory.java
index d95f405..37cb2b3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/FileSystemFactory.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/file/FileSystemFactory.java
@@ -21,23 +21,30 @@ package org.apache.sshd.common.file;
 
 import java.io.IOException;
 import java.nio.file.FileSystem;
+import java.nio.file.Path;
 
-import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionContext;
 
 /**
  * Factory for file system implementations - it returns the file system for 
user.
  *
  * @author <a href="http://mina.apache.org";>Apache MINA Project</a>
  */
-@FunctionalInterface
 public interface FileSystemFactory {
+    /**
+     *
+     * @param  session     The session created for the user
+     * @return             The recommended user home directory - {@code null} 
if none
+     * @throws IOException If failed to resolve user's home directory
+     */
+    Path getUserHomeDir(SessionContext session) throws IOException;
 
     /**
      * Create user specific file system.
      *
      * @param  session     The session created for the user
      * @return             The current {@link FileSystem} for the provided 
session
-     * @throws IOException if the filesystem can not be created
+     * @throws IOException if the file system can not be created
      */
-    FileSystem createFileSystem(Session session) throws IOException;
+    FileSystem createFileSystem(SessionContext session) throws IOException;
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java
similarity index 78%
rename from 
sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java
rename to 
sshd-common/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java
index 2343851..784fc94 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java
@@ -23,12 +23,15 @@ import java.io.IOException;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
 import java.nio.file.NotDirectoryException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
 import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
 
@@ -38,7 +41,7 @@ import 
org.apache.sshd.common.util.logging.AbstractLoggingBean;
  * @author <a href="http://mina.apache.org";>Apache MINA Project</a>
  */
 public class NativeFileSystemFactory extends AbstractLoggingBean implements 
FileSystemFactory {
-    public static final String DEFAULT_USERS_HOME = "/home";
+    public static final String DEFAULT_USERS_HOME = OsUtils.isWin32() ? 
"C:\\Users" : "/home";
 
     public static final NativeFileSystemFactory INSTANCE = new 
NativeFileSystemFactory();
 
@@ -90,12 +93,29 @@ public class NativeFileSystemFactory extends 
AbstractLoggingBean implements File
     }
 
     @Override
-    public FileSystem createFileSystem(Session session) throws IOException {
+    public Path getUserHomeDir(SessionContext session) throws IOException {
         String userName = session.getUsername();
+        if (GenericUtils.isEmpty(userName)) {
+            return null;
+        }
+
+        String homeRoot = getUsersHomeDir();
+        if (GenericUtils.isEmpty(homeRoot)) {
+            return null;
+        }
+
+        return Paths.get(homeRoot, userName).normalize().toAbsolutePath();
+    }
+
+    @Override
+    public FileSystem createFileSystem(SessionContext session) throws 
IOException {
         // create home if does not exist
         if (isCreateHome()) {
-            String homeRoot = getUsersHomeDir();
-            Path homeDir = Paths.get(homeRoot, 
userName).normalize().toAbsolutePath();
+            Path homeDir = getUserHomeDir(session);
+            if (homeDir == null) {
+                throw new InvalidPathException(session.getUsername(), "Cannot 
resolve home directory");
+            }
+
             if (Files.exists(homeDir)) {
                 if (!Files.isDirectory(homeDir)) {
                     throw new NotDirectoryException(homeDir.toString());
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemFactory.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemFactory.java
similarity index 87%
rename from 
sshd-core/src/main/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemFactory.java
rename to 
sshd-common/src/main/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemFactory.java
index 5dd87bf..7ad53e5 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemFactory.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemFactory.java
@@ -29,7 +29,7 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.root.RootedFileSystemProvider;
-import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.util.ValidateUtils;
 
 /**
@@ -66,17 +66,7 @@ public class VirtualFileSystemFactory implements 
FileSystemFactory {
     }
 
     @Override
-    public FileSystem createFileSystem(Session session) throws IOException {
-        String username = session.getUsername();
-        Path dir = computeRootDir(session);
-        if (dir == null) {
-            throw new InvalidPathException(username, "Cannot resolve home 
directory");
-        }
-
-        return new RootedFileSystemProvider().newFileSystem(dir, 
Collections.emptyMap());
-    }
-
-    protected Path computeRootDir(Session session) throws IOException {
+    public Path getUserHomeDir(SessionContext session) throws IOException {
         String username = session.getUsername();
         Path homeDir = getUserHomeDir(username);
         if (homeDir == null) {
@@ -85,4 +75,14 @@ public class VirtualFileSystemFactory implements 
FileSystemFactory {
 
         return homeDir;
     }
+
+    @Override
+    public FileSystem createFileSystem(SessionContext session) throws 
IOException {
+        Path dir = getUserHomeDir(session);
+        if (dir == null) {
+            throw new InvalidPathException(session.getUsername(), "Cannot 
resolve home directory");
+        }
+
+        return new RootedFileSystemProvider().newFileSystem(dir, 
Collections.emptyMap());
+    }
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java 
b/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
index 3303584..132c5d9 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
@@ -760,7 +760,7 @@ public class ChannelSession extends AbstractServerChannel {
         if (command instanceof FileSystemAware) {
             ServerFactoryManager manager = ((ServerSession) 
session).getFactoryManager();
             FileSystemFactory factory = manager.getFileSystemFactory();
-            ((FileSystemAware) 
command).setFileSystem(factory.createFileSystem(session));
+            ((FileSystemAware) command).setFileSystemFactory(factory, session);
         }
         // If the shell wants to use non-blocking io
         if (command instanceof AsyncCommand) {
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
index ac4af52..c215986 100644
--- 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
@@ -18,15 +18,15 @@
  */
 package org.apache.sshd.server.scp;
 
-import java.io.File;
 import java.io.IOError;
 import java.io.IOException;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
 import java.io.Reader;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
 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;
@@ -36,11 +36,12 @@ import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -48,13 +49,18 @@ import java.util.TreeMap;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.file.FileSystemFactory;
 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.session.SessionContext;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.threads.CloseableExecutorService;
+import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.AbstractFileSystemCommand;
 
@@ -64,22 +70,42 @@ import 
org.apache.sshd.server.command.AbstractFileSystemCommand;
  * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
  */
 public class ScpShell extends AbstractFileSystemCommand {
+    /**
+     * Used to indicate the {@link Charset} (or its name) for encoding 
referenced files/folders names - extracted from
+     * the client channel session when 1st initialized.
+     *
+     * @see #DEFAULT_NAME_ENCODING_CHARSET
+     */
+    public static final String NAME_ENCODING_CHARSET = 
"scp-shell-name-encoding-charset";
+
+    /**
+     * Default value of {@value #NAME_ENCODING_CHARSET}
+     */
+    public static final Charset DEFAULT_NAME_ENCODING_CHARSET = 
StandardCharsets.UTF_8;
 
     public static final String STATUS = "status";
 
-    protected static final boolean IS_WINDOWS = 
System.getProperty("os.name").toLowerCase().contains("win");
+    /** The &quot;PWD&quot; environment variable */
+    public static final String ENV_PWD = "PWD";
 
-    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];
+    /** The &quot;HOME&quot; environment variable */
+    public static final String ENV_HOME = "HOME";
+
+    /**
+     * Key for the language - format &quot;en_US.UTF-8&quot;
+     */
+    public static final String ENV_LANG = "LANG";
 
     protected final ChannelSession channel;
-    protected ScpFileOpener opener;
-    protected ScpTransferEventListener listener;
-    protected int sendBufferSize;
-    protected int receiveBufferSize;
+    protected final Map<String, Object> variables = new HashMap<>();
+    protected final Charset nameEncodingCharset;
+
+    protected final ScpFileOpener opener;
+    protected final ScpTransferEventListener listener;
+    protected final int sendBufferSize;
+    protected final int receiveBufferSize;
     protected Path currentDir;
-    protected Map<String, Object> variables = new HashMap<>();
+    protected Path homeDir;
 
     public ScpShell(ChannelSession channel, CloseableExecutorService 
executorService,
                     int sendSize, int receiveSize,
@@ -87,6 +113,9 @@ public class ScpShell extends AbstractFileSystemCommand {
         super(null, executorService);
         this.channel = channel;
 
+        nameEncodingCharset = PropertyResolverUtils.getCharset(
+                channel, NAME_ENCODING_CHARSET, DEFAULT_NAME_ENCODING_CHARSET);
+
         if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
             throw new IllegalArgumentException(
                     "<ScpShell> send buffer size "
@@ -105,42 +134,94 @@ public class ScpShell extends AbstractFileSystemCommand {
 
         opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : 
fileOpener;
         listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : 
eventListener;
+    }
 
+    @Override
+    public void setFileSystemFactory(FileSystemFactory factory, SessionContext 
session) throws IOException {
+        homeDir = factory.getUserHomeDir(session);
+        super.setFileSystemFactory(factory, session);
     }
 
-    protected void println(Object x, OutputStream out) {
+    protected void println(
+            String cmd, Object x, OutputStream out, Charset cs) {
         try {
-            String s = x + System.lineSeparator();
-            out.write(s.getBytes());
+            String s = x.toString();
+            if (log.isDebugEnabled()) {
+                log.debug("println({})[{}]: {}",
+                        channel, cmd, s.replace('\n', ' ').replace('\t', ' '));
+            }
+            out.write(s.getBytes(cs));
+            // always write LF even if running on Windows
+            out.write('\n');
         } catch (IOException e) {
             throw new IOError(e);
         }
     }
 
+    protected void signalError(String cmd, String errorMsg) {
+        signalError(cmd, errorMsg, StandardCharsets.US_ASCII);
+    }
+
+    protected void signalError(String cmd, String errorMsg, Charset cs) {
+        log.warn("{}[{}]: {}", channel, cmd, errorMsg);
+        println(cmd, errorMsg, getErrorStream(), cs);
+        variables.put(STATUS, 1);
+    }
+
     @Override
     public void run() {
         String command = null;
+        variables.put(STATUS, 0);
+
+        boolean debugEnabled = log.isDebugEnabled();
         try {
-            currentDir = opener.resolveLocalPath(channel.getSession(), 
fileSystem, ".");
+            // TODO find some better alternative
+            if (homeDir == null) {
+                currentDir = opener.resolveLocalPath(channel.getSession(), 
fileSystem, ".");
+                log.warn("run - no home dir - starting at {}", currentDir);
+            } else {
+                currentDir = homeDir;
+                if (debugEnabled) {
+                    log.debug("run - starting at home dir={}", homeDir);
+                }
+            }
+
+            prepareEnvironment(getEnvironment());
+
             // 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 (;;) {
+                for (int executedCommands = 0;; executedCommands++) {
                     command = readLine(r);
-                    if (command.length() == 0 || !handleCommandLine(command)) {
+                    if (GenericUtils.isEmpty(command)) {
+                        if (debugEnabled) {
+                            log.debug("run({}) Command loop terminated after 
{} commands", channel, executedCommands);
+                        }
+
+                        return;
+                    }
+
+                    if (!handleCommandLine(command)) {
+                        if (debugEnabled) {
+                            log.debug("run({}) Command loop terminated by 
cmd={} after {} commands",
+                                    channel, command, executedCommands);
+                        }
                         return;
                     }
                 }
             }
         } catch (InterruptedIOException e) {
-            // Ignore - signaled end
+            if (debugEnabled) {
+                log.debug("run({}) interrupted after command={}", channel, 
command);
+            }
         } catch (Exception e) {
             String message = "Failed (" + e.getClass().getSimpleName() + ") to 
handle '" + command + "': " + e.getMessage();
+            log.warn("run({}) {}", channel, message);
             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());
+                log.warn("run({}) Failed ({}) to write error message={}: {}",
+                        channel, ioe.getClass().getSimpleName(), message, 
ioe.getMessage());
             } finally {
                 onExit(-1, message);
             }
@@ -153,16 +234,29 @@ public class ScpShell extends AbstractFileSystemCommand {
         StringBuilder sb = new StringBuilder();
         while (true) {
             int c = reader.read();
-            if (c < 0 || c == '\n') {
+            if ((c < 0) || c == '\n') {
                 break;
             }
             sb.append((char) c);
         }
+
+        int len = sb.length();
+        // Strip CR at end of line if present
+        if ((len > 0) && (sb.charAt(len - 1) == '\r')) {
+            sb.setLength(len - 1);
+        }
+
         return sb.toString();
     }
 
     protected boolean handleCommandLine(String command) throws Exception {
+        if (log.isDebugEnabled()) {
+            log.debug("handleCommandLine({}) {}", channel, command);
+        }
+
         List<String[]> cmds = parse(command);
+        OutputStream stdout = getOutputStream();
+        OutputStream stderr = getErrorStream();
         for (String[] argv : cmds) {
             switch (argv[0]) {
                 case "echo":
@@ -183,21 +277,44 @@ public class ScpShell extends AbstractFileSystemCommand {
                 case "groups":
                     variables.put(STATUS, 0);
                     break;
+                case "printenv":
+                    printenv(argv);
+                    break;
                 case "unset":
+                    unset(argv);
+                    break;
                 case "unalias":
-                case "printenv":
                     variables.put(STATUS, 1);
                     break;
                 default:
-                    variables.put(STATUS, 127);
-                    getErrorStream().write(("command not found: " + argv[0] + 
"\n").getBytes());
+                    handleUnsupportedCommand(command, argv);
             }
-            getOutputStream().flush();
-            getErrorStream().flush();
+            stdout.flush();
+            stderr.flush();
         }
+
         return true;
     }
 
+    protected void prepareEnvironment(Environment environ) {
+        Map<String, String> env = environ.getEnv();
+        Locale locale = Locale.getDefault();
+        String languageTag = locale.toLanguageTag();
+        env.put(ENV_LANG, languageTag.replace('-', '_') + "." + 
nameEncodingCharset.displayName());
+
+        if (homeDir != null) {
+            env.put(ENV_HOME, homeDir.toString());
+        }
+
+        updatePwdEnvVariable(currentDir);
+    }
+
+    protected void handleUnsupportedCommand(String command, String[] argv) 
throws Exception {
+        log.warn("handleUnsupportedCommand({}) unsupported: {}", channel, 
command);
+        variables.put(STATUS, 127);
+        getErrorStream().write(("command not found: " + argv[0] + 
"\n").getBytes(StandardCharsets.US_ASCII));
+    }
+
     protected List<String[]> parse(String command) {
         List<String[]> cmds = new ArrayList<>();
         List<String> args = new ArrayList<>();
@@ -243,6 +360,58 @@ public class ScpShell extends AbstractFileSystemCommand {
         return cmds;
     }
 
+    protected void printenv(String[] argv) throws Exception {
+        Environment environ = getEnvironment();
+        Map<String, String> envValues = environ.getEnv();
+        OutputStream stdout = getOutputStream();
+        if (argv.length == 1) {
+            envValues.entrySet()
+                    .stream()
+                    .forEach(e -> println(argv[0], e.getKey() + "=" + 
e.getValue(), stdout, StandardCharsets.US_ASCII));
+            variables.put(STATUS, 0);
+            return;
+        }
+
+        if (argv.length != 2) {
+            signalError(argv[0], "printenv: only one variable value at a 
time");
+            return;
+        }
+
+        String varName = argv[1];
+        String varValue = resolveEnvironmentVariable(varName, envValues);
+        if (varValue == null) {
+            signalError(argv[0], "printenv: variable not set " + varName);
+            return;
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("printenv({}) {}={}", channel, varName, varValue);
+        }
+
+        println(argv[0], varValue, stdout, StandardCharsets.US_ASCII);
+        variables.put(STATUS, 0);
+    }
+
+    protected String resolveEnvironmentVariable(String varName, Map<String, 
String> envValues) {
+        return envValues.get(varName);
+    }
+
+    protected void unset(String[] argv) throws Exception {
+        if (argv.length != 2) {
+            signalError(argv[0], "unset: exactly one argument is expected");
+            return;
+        }
+
+        Environment environ = getEnvironment();
+        Map<String, String> envValues = environ.getEnv();
+        String varName = argv[1];
+        String varValue = envValues.remove(varName);
+        if (log.isDebugEnabled()) {
+            log.debug("unset({}) {}={}", channel, varName, varValue);
+        }
+        variables.put(STATUS, (varValue == null) ? 1 : 0);
+    }
+
     protected void scp(String[] argv) throws Exception {
         boolean optR = false;
         boolean optT = false;
@@ -252,62 +421,81 @@ public class ScpShell extends AbstractFileSystemCommand {
         boolean isOption = true;
         String path = null;
         for (int i = 1; i < argv.length; i++) {
-            if (isOption && argv[i].startsWith("-")) {
-                switch (argv[i]) {
-                    case "-r":
+            String argVal = argv[i];
+            if (GenericUtils.isEmpty(argVal)) {
+                signalError(argv[0], "scp: empty argument not allowed");
+                return;
+            }
+
+            if (isOption && (argVal.charAt(0) == '-')) {
+                if (argVal.length() != 2) {
+                    signalError(argv[0], "scp: only one option at a time may 
be specified");
+                    return;
+                }
+
+                // TODO should we raise an error if option re-specified ?
+                char optVal = argVal.charAt(1);
+                switch (optVal) {
+                    case 'r':
                         optR = true;
                         break;
-                    case "-t":
+                    case 't':
                         optT = true;
                         break;
-                    case "-f":
+                    case 'f':
                         optF = true;
                         break;
-                    case "-d":
+                    case 'd':
                         optD = true;
                         break;
-                    case "-p":
+                    case 'p':
                         optP = true;
                         break;
                     default:
-                        println("scp: unsupported option: " + argv[i], 
getErrorStream());
-                        variables.put(STATUS, 1);
+                        signalError(argv[0], "scp: unsupported option: " + 
argVal);
                         return;
                 }
             } else if (path == null) {
-                path = argv[i];
+                path = argVal;
                 isOption = false;
             } else {
-                println("scp: one and only one argument expected", 
getErrorStream());
-                variables.put(STATUS, 1);
+                signalError(argv[0], "scp: one and only one path argument 
expected");
                 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);
+
+        if ((optT && optF) || (!optT && !optF)) {
+            signalError(argv[0], "scp: one and only one of -t and -f option 
expected");
+            return;
+        }
+
+        doScp(path, optR, optT, optF, optD, optP);
+    }
+
+    protected void doScp(
+            String path, boolean optR, boolean optT, boolean optF, boolean 
optD, boolean optP)
+            throws Exception {
+        try {
+            ScpHelper helper = new ScpHelper(
+                    channel.getSession(), getInputStream(), getOutputStream(),
+                    fileSystem, opener, listener);
+            Path localPath = currentDir.resolve(path);
+            if (optT) {
+                helper.receive(localPath, optR, optD, optP, receiveBufferSize);
+            } else {
+                helper.send(Collections.singletonList(localPath.toString()), 
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);
         }
     }
 
@@ -352,87 +540,137 @@ public class ScpShell extends AbstractFileSystemCommand {
                 }
             }
         }
-        println(buf, getOutputStream());
+        println(argv[0], buf, getOutputStream(), nameEncodingCharset);
         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);
+            signalError(argv[0], "pwd: too many arguments");
         } else {
-            println(currentDir, getOutputStream());
+            println(argv[0], currentDir, getOutputStream(), 
nameEncodingCharset);
             variables.put(STATUS, 0);
         }
     }
 
     protected void cd(String[] argv) throws Exception {
+        if (argv.length == 1) {
+            if (homeDir != null) {
+                currentDir = homeDir;
+                updatePwdEnvVariable(currentDir);
+                variables.put(STATUS, 0);
+            } else {
+                signalError(argv[0], "No home directory to return to");
+            }
+
+            return;
+        }
+
         if (argv.length != 2) {
-            println("cd: too many or too few arguments", getErrorStream());
-            variables.put(STATUS, 1);
+            signalError(argv[0], "cd: too many or too few arguments");
+            return;
+        }
+
+        String path = argv[1];
+        if (GenericUtils.isEmpty(path)) {
+            signalError(argv[0], "cd: empty target");
+            return;
+        }
+
+        // TODO make sure not escaping the user's sandbox filesystem
+        Path cwd = currentDir;
+        cwd = cwd.resolve(path).toAbsolutePath().normalize();
+        if (!Files.exists(cwd)) {
+            signalError(argv[0], "no such file or directory: " + path, 
nameEncodingCharset);
+        } else if (!Files.isDirectory(cwd)) {
+            signalError(argv[0], "not a directory: " + path, 
nameEncodingCharset);
         } 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);
+            if (log.isDebugEnabled()) {
+                log.debug("cd - {} => {}", currentDir, cwd);
             }
+            currentDir = cwd;
+            updatePwdEnvVariable(currentDir);
+            variables.put(STATUS, 0);
         }
     }
 
+    protected void updatePwdEnvVariable(Path pwd) {
+        Environment environ = getEnvironment();
+        Map<String, String> envVars = environ.getEnv();
+        envVars.put(ENV_PWD, pwd.toString());
+    }
+
     protected void ls(String[] argv) throws Exception {
         // find options
-        boolean a = false;
-        boolean l = false;
-        boolean f = false;
+        boolean optListAll = false;
+        boolean optLong = false;
+        boolean optFullTime = 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)) {
+            String argValue = argv[k];
+            if (GenericUtils.isEmpty(argValue)) {
+                signalError(argv[0], "ls: empty argument not allowed");
+                return;
+            }
+
+            if (argValue.equals("--full-time")) {
+                optFullTime = true;
+            } else if (argValue.charAt(0) == '-') {
+                int argLen = argValue.length();
+                if (argLen == 1) {
+                    signalError(argv[0], "ls: no option specified");
+                    return;
+                }
+
+                for (int i = 1; i < argLen; i++) {
+                    char optValue = argValue.charAt(i);
+                    // TODO should we raise an error if option re-specified ?
+                    switch (optValue) {
                         case 'a':
-                            a = true;
+                            optListAll = true;
                             break;
                         case 'l':
-                            l = true;
+                            optLong = true;
                             break;
                         default:
-                            println("unsupported option: -" + 
argv[k].charAt(i), getErrorStream());
-                            variables.put(STATUS, 1);
+                            signalError(argv[0], "unsupported option: -" + 
optValue);
                             return;
                     }
                 }
             } else {
-                println("unsupported option: " + argv[k], getErrorStream());
-                variables.put(STATUS, 1);
+                signalError(argv[0], "unsupported option: " + argValue);
                 return;
             }
         }
-        boolean optListAll = a;
-        boolean optLong = l;
-        boolean optFullTime = f;
+
+        doLs(argv, optListAll, optLong, optFullTime);
+    }
+
+    protected void doLs(
+            String argv[], boolean optListAll, boolean optLong, boolean 
optFullTime)
+            throws Exception {
         // list current directory content
-        Predicate<Path> filter = p -> optListAll || 
p.getFileName().toString().equals(".")
-                || p.getFileName().toString().equals("..") || 
!p.getFileName().toString().startsWith(".");
+        Predicate<Path> filter = p -> {
+            String fileName = p.getFileName().toString();
+            return optListAll || fileName.equals(".")
+                    || fileName.equals("..") || !fileName.startsWith(".");
+        };
+
+        // TODO make sure not listing above user's home directory
         String[] synth = currentDir.toString().equals("/") ? new String[] { 
"." } : new String[] { ".", ".." };
+        OutputStream stdout = getOutputStream();
         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()));
+                .forEach(str -> println(argv[0], str, stdout, 
nameEncodingCharset));
         variables.put(STATUS, 0);
     }
 
     protected static class PathEntry implements Comparable<PathEntry> {
+        public static final DateTimeFormatter FULL_TIME_VALUE_FORMATTER = 
DateTimeFormatter.ofPattern("MMM ppd HH:mm:ss yyyy");
+        public static final DateTimeFormatter TIME_ONLY_VALUE_FORMATTER = 
DateTimeFormatter.ofPattern("MMM ppd HH:mm");
+        public static final DateTimeFormatter YEAR_VALUE_FORMATTER = 
DateTimeFormatter.ofPattern("MMM ppd  yyyy");
 
         protected final Path abs;
         protected final Path path;
@@ -449,68 +687,80 @@ public class ScpShell extends AbstractFileSystemCommand {
             return path.toString().compareTo(o.path.toString());
         }
 
+        @Override
+        public String toString() {
+            return Objects.toString(abs);
+        }
+
         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();
+            String abbrev = shortDisplay();
+            if (!optLongDisplay) {
+                return abbrev;
+            }
+
+            StringBuilder sb = new StringBuilder(abbrev.length() + 64);
+            if (is("isDirectory")) {
+                sb.append('d');
+            } else if (is("isSymbolicLink")) {
+                sb.append('l');
+            } else if (is("isOther")) {
+                sb.append('o');
             } else {
-                return shortDisplay();
+                sb.append('-');
+            }
+
+            @SuppressWarnings("unchecked")
+            Set<PosixFilePermission> perms = (Set<PosixFilePermission>) 
attributes.get("permissions");
+            if (perms == null) {
+                perms = EnumSet.noneOf(PosixFilePermission.class);
             }
+            sb.append(PosixFilePermissions.toString(perms));
+
+            Object nlinkValue = attributes.get("nlink");
+            sb.append(' ').append(String.format("%3s", (nlinkValue != null) ? 
nlinkValue : "1"));
+
+            appendOwnerInformation(sb, "owner", "owner");
+            appendOwnerInformation(sb, "group", "group");
+
+            Number length = (Number) attributes.get("size");
+            if (length == null) {
+                length = 0L;
+            }
+            sb.append(' ').append(String.format("%1$8s", length));
+
+            String timeValue = toString((FileTime) 
attributes.get("lastModifiedTime"), optFullTime);
+            sb.append(' ').append(timeValue);
+
+            sb.append(' ').append(abbrev);
+            return sb.toString();
         }
 
         protected boolean is(String attr) {
             Object d = attributes.get(attr);
-            return d instanceof Boolean && (Boolean) d;
+            return (d instanceof Boolean) && (Boolean) d;
+        }
+
+        protected StringBuilder appendOwnerInformation(
+                StringBuilder sb, String attr, String defaultValue) {
+            String owner = Objects.toString(attributes.get(attr), null);
+            if (GenericUtils.isEmpty(owner)) {
+                owner = defaultValue;
+            }
+            if (owner.length() > 8) {
+                owner = owner.substring(0, 8);
+            }
+            sb.append(' ').append(owner);
+            for (int index = owner.length(); index < 8; index++) {
+                sb.append(' ');
+            }
+            return sb;
         }
 
         protected String shortDisplay() {
             if (is("isSymbolicLink")) {
                 try {
                     Path l = Files.readSymbolicLink(abs);
-                    return path.toString() + " -> " + l.toString();
+                    return path + " -> " + l;
                 } catch (IOException e) {
                     // ignore
                 }
@@ -518,80 +768,38 @@ public class ScpShell extends AbstractFileSystemCommand {
             return path.toString();
         }
 
-        protected String toString(FileTime time, boolean optFullTime) {
+        protected static 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);
+                return FULL_TIME_VALUE_FORMATTER.format(dt);
             } else if (System.currentTimeMillis() - millis < 183L * 24L * 60L 
* 60L * 1000L) {
-                return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt);
+                return TIME_ONLY_VALUE_FORMATTER.format(dt);
             } else {
-                return DateTimeFormatter.ofPattern("MMM ppd  yyyy").format(dt);
+                return YEAR_VALUE_FORMATTER.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()) {
+            FileSystem fs = path.getFileSystem();
+            Collection<String> views = fs.supportedFileAttributeViews();
+            for (String view : views) {
                 try {
-                    Map<String, Object> ta = Files.readAttributes(path, view + 
":*", EMPTY_LINK_OPTIONS);
+                    Map<String, Object> ta = Files.readAttributes(
+                            path, view + ":*", IoUtils.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()));
+            attrs.computeIfAbsent("permissions", s -> 
IoUtils.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-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java 
b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
index e2c05b6..e155ff1 100644
--- 
a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
+++ 
b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
@@ -74,8 +74,10 @@ import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.channel.WindowClosedException;
 import org.apache.sshd.common.channel.exception.SshChannelClosedException;
+import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
 import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.subsystem.sftp.SftpException;
 import 
org.apache.sshd.common.subsystem.sftp.extensions.AclSupportedParser.AclCapabilities;
@@ -254,7 +256,17 @@ public class SftpTest extends 
AbstractSftpClientTestSupport {
     public void testNavigateBeyondRootFolder() throws Exception {
         Path rootLocation = Paths.get(OsUtils.isUNIX() ? "/" : "C:\\");
         FileSystem fsRoot = rootLocation.getFileSystem();
-        sshd.setFileSystemFactory(session1 -> fsRoot);
+        sshd.setFileSystemFactory(new FileSystemFactory() {
+            @Override
+            public Path getUserHomeDir(SessionContext session) throws 
IOException {
+                return rootLocation;
+            }
+
+            @Override
+            public FileSystem createFileSystem(SessionContext session) throws 
IOException {
+                return fsRoot;
+            }
+        });
 
         try (ClientSession session = client.connect(getCurrentTestName(), 
TEST_LOCALHOST, port)
                 .verify(CONNECT_TIMEOUT).getSession()) {

Reply via email to