/*
 * Copyright (c) 2002-2018 ymnk, JCraft,Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted
 * provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The names of the authors may not be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jcraft.jsch;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Hashtable;
import java.util.Vector;

public class ChannelSftp extends ChannelSession {

  private static final int LOCAL_MAXIMUM_PACKET_SIZE = 32 * 1024;
  private static final int LOCAL_WINDOW_SIZE_MAX = (64 * LOCAL_MAXIMUM_PACKET_SIZE);

  private static final byte SSH_FXP_INIT = 1;
  private static final byte SSH_FXP_VERSION = 2;
  private static final byte SSH_FXP_OPEN = 3;
  private static final byte SSH_FXP_CLOSE = 4;
  private static final byte SSH_FXP_READ = 5;
  private static final byte SSH_FXP_WRITE = 6;
  private static final byte SSH_FXP_LSTAT = 7;
  private static final byte SSH_FXP_FSTAT = 8;
  private static final byte SSH_FXP_SETSTAT = 9;
  private static final byte SSH_FXP_FSETSTAT = 10;
  private static final byte SSH_FXP_OPENDIR = 11;
  private static final byte SSH_FXP_READDIR = 12;
  private static final byte SSH_FXP_REMOVE = 13;
  private static final byte SSH_FXP_MKDIR = 14;
  private static final byte SSH_FXP_RMDIR = 15;
  private static final byte SSH_FXP_REALPATH = 16;
  private static final byte SSH_FXP_STAT = 17;
  private static final byte SSH_FXP_RENAME = 18;
  private static final byte SSH_FXP_READLINK = 19;
  private static final byte SSH_FXP_SYMLINK = 20;
  private static final byte SSH_FXP_STATUS = 101;
  private static final byte SSH_FXP_HANDLE = 102;
  private static final byte SSH_FXP_DATA = 103;
  private static final byte SSH_FXP_NAME = 104;
  private static final byte SSH_FXP_ATTRS = 105;
  private static final byte SSH_FXP_EXTENDED = (byte) 200;
  private static final byte SSH_FXP_EXTENDED_REPLY = (byte) 201;

  // pflags
  private static final int SSH_FXF_READ = 0x00000001;
  private static final int SSH_FXF_WRITE = 0x00000002;
  private static final int SSH_FXF_APPEND = 0x00000004;
  private static final int SSH_FXF_CREAT = 0x00000008;
  private static final int SSH_FXF_TRUNC = 0x00000010;
  private static final int SSH_FXF_EXCL = 0x00000020;

  private static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001;
  private static final int SSH_FILEXFER_ATTR_UIDGID = 0x00000002;
  private static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004;
  private static final int SSH_FILEXFER_ATTR_ACMODTIME = 0x00000008;
  private static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000;

  public static final int SSH_FX_OK = 0;
  public static final int SSH_FX_EOF = 1;
  public static final int SSH_FX_NO_SUCH_FILE = 2;
  public static final int SSH_FX_PERMISSION_DENIED = 3;
  public static final int SSH_FX_FAILURE = 4;
  public static final int SSH_FX_BAD_MESSAGE = 5;
  public static final int SSH_FX_NO_CONNECTION = 6;
  public static final int SSH_FX_CONNECTION_LOST = 7;
  public static final int SSH_FX_OP_UNSUPPORTED = 8;
  /*
   * SSH_FX_OK Indicates successful completion of the operation. SSH_FX_EOF indicates end-of-file
   * condition; for SSH_FX_READ it means that no more data is available in the file, and for
   * SSH_FX_READDIR it indicates that no more files are contained in the directory.
   * SSH_FX_NO_SUCH_FILE is returned when a reference is made to a file which should exist but
   * doesn't. SSH_FX_PERMISSION_DENIED is returned when the authenticated user does not have
   * sufficient permissions to perform the operation. SSH_FX_FAILURE is a generic catch-all error
   * message; it should be returned if an error occurs for which there is no more specific error
   * code defined. SSH_FX_BAD_MESSAGE may be returned if a badly formatted packet or protocol
   * incompatibility is detected. SSH_FX_NO_CONNECTION is a pseudo-error which indicates that the
   * client has no connection to the server (it can only be generated locally by the client, and
   * MUST NOT be returned by servers). SSH_FX_CONNECTION_LOST is a pseudo-error which indicates that
   * the connection to the server has been lost (it can only be generated locally by the client, and
   * MUST NOT be returned by servers). SSH_FX_OP_UNSUPPORTED indicates that an attempt was made to
   * perform an operation which is not supported for the server (it may be generated locally by the
   * client if e.g. the version number exchange indicates that a required feature is not supported
   * by the server, or it may be returned by the server if the server does not implement an
   * operation).
   */
  private static final int MAX_MSG_LENGTH = 256 * 1024;

  public static final int OVERWRITE = 0;
  public static final int RESUME = 1;
  public static final int APPEND = 2;

  private boolean interactive = false;
  private int seq = 1;
  private int[] ackid = new int[1];

  private Buffer buf;
  private Packet packet;

  // The followings will be used in file uploading.
  private Buffer obuf;
  private Packet opacket;

  private int client_version = 3;
  private int server_version = 3;
  private String version = String.valueOf(client_version);

  private Hashtable<String, String> extensions = null;
  private InputStream io_in = null;

  private boolean extension_posix_rename = false;
  private boolean extension_statvfs = false;
  // private boolean extension_fstatvfs = false;
  private boolean extension_hardlink = false;

  /*
   * 10. Changes from previous protocol versions The SSH File Transfer Protocol has changed over
   * time, before it's standardization. The following is a description of the incompatible changes
   * between different versions. 10.1 Changes between versions 3 and 2 o The SSH_FXP_READLINK and
   * SSH_FXP_SYMLINK messages were added. o The SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY messages
   * were added. o The SSH_FXP_STATUS message was changed to include fields `error message' and
   * `language tag'. 10.2 Changes between versions 2 and 1 o The SSH_FXP_RENAME message was added.
   * 10.3 Changes between versions 1 and 0 o Implementation changes, no actual protocol changes.
   */

  private static final String file_separator = File.separator;
  private static final char file_separatorc = File.separatorChar;
  private static boolean fs_is_bs = (byte) File.separatorChar == '\\';

  private String cwd;
  private String home;
  private String lcwd;

  private Charset fEncoding = StandardCharsets.UTF_8;
  private boolean fEncoding_is_utf8 = true;

  private boolean useWriteFlushWorkaround = true;

  private RequestQueue rq = new RequestQueue(16);

  /**
   * Specify how many requests may be sent at any one time. Increasing this value may slightly
   * improve file transfer speed but will increase memory usage. The default is 16 requests.
   *
   * @param bulk_requests how many requests may be outstanding at any one time.
   */
  public void setBulkRequests(int bulk_requests) throws JSchException {
    if (bulk_requests > 0)
      rq = new RequestQueue(bulk_requests);
    else
      throw new JSchException("setBulkRequests: " + bulk_requests + " must be greater than 0.");
  }

  /**
   * This method will return the value how many requests may be sent at any one time.
   *
   * @return how many requests may be sent at any one time.
   */
  public int getBulkRequests() {
    return rq.size();
  }

  public void setUseWriteFlushWorkaround(boolean useWriteFlushWorkaround) {
    this.useWriteFlushWorkaround = useWriteFlushWorkaround;
  }

  public boolean getUseWriteFlushWorkaround() {
    return useWriteFlushWorkaround;
  }

  public ChannelSftp() {
    super();
    lwsize_max = LOCAL_WINDOW_SIZE_MAX;
    lwsize = LOCAL_WINDOW_SIZE_MAX;
    lmpsize = LOCAL_MAXIMUM_PACKET_SIZE;
  }

  @Override
  void init() {}

  @Override
  public void start() throws JSchException {
    try {

      PipedOutputStream pos = new PipedOutputStream();
      io.setOutputStream(pos);
      PipedInputStream pis = new MyPipedInputStream(pos, rq.size() * rmpsize);
      io.setInputStream(pis);

      io_in = io.in;

      if (io_in == null) {
        throw new JSchException("channel is down");
      }

      Request request = new RequestSftp();
      request.request(getSession(), this);

      /*
       * System.err.println("lmpsize: "+lmpsize); System.err.println("lwsize: "+lwsize);
       * System.err.println("rmpsize: "+rmpsize); System.err.println("rwsize: "+rwsize);
       */

      buf = new Buffer(lmpsize);
      packet = new Packet(buf);

      obuf = new Buffer(rmpsize);
      opacket = new Packet(obuf);

      int i = 0;
      int length;
      int type;
      byte[] str;

      // send SSH_FXP_INIT
      sendINIT();

      // receive SSH_FXP_VERSION
      Header header = new Header();
      header = header(buf, header);
      length = header.length;
      if (length > MAX_MSG_LENGTH) {
        throw new SftpException(SSH_FX_FAILURE, "Received message is too long: " + length);
      }
      type = header.type; // 2 -> SSH_FXP_VERSION
      server_version = header.rid;
      // System.err.println("SFTP protocol server-version="+server_version);
      extensions = new Hashtable<>();
      if (length > 0) {
        // extension data
        fill(buf, length);
        byte[] extension_name = null;
        byte[] extension_data = null;
        while (length > 0) {
          extension_name = buf.getString();
          length -= (4 + extension_name.length);
          extension_data = buf.getString();
          length -= (4 + extension_data.length);
          extensions.put(Util.byte2str(extension_name), Util.byte2str(extension_data));
        }
      }

      if (extensions.get("posix-rename@openssh.com") != null
          && extensions.get("posix-rename@openssh.com").equals("1")) {
        extension_posix_rename = true;
      }

      if (extensions.get("statvfs@openssh.com") != null
          && extensions.get("statvfs@openssh.com").equals("2")) {
        extension_statvfs = true;
      }

      /*
       * if(extensions.get("fstatvfs@openssh.com")!=null &&
       * extensions.get("fstatvfs@openssh.com").equals("2")){ extension_fstatvfs = true; }
       */

      if (extensions.get("hardlink@openssh.com") != null
          && extensions.get("hardlink@openssh.com").equals("1")) {
        extension_hardlink = true;
      }

      lcwd = new File(".").getCanonicalPath();
    } catch (Exception e) {
      // System.err.println(e);
      if (e instanceof JSchException)
        throw (JSchException) e;
      throw new JSchException(e.toString(), e);
    }
  }

  public void quit() {
    disconnect();
  }

  public void exit() {
    disconnect();
  }

  public void lcd(String path) throws SftpException {
    path = localAbsolutePath(path);
    if ((new File(path)).isDirectory()) {
      try {
        path = (new File(path)).getCanonicalPath();
      } catch (Exception e) {
      }
      lcwd = path;
      return;
    }
    throw new SftpException(SSH_FX_NO_SUCH_FILE, "No such directory");
  }

  public void cd(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);
      path = isUnique(path);

      byte[] str = _realpath(path);
      SftpATTRS attr = _stat(str);

      if ((attr.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_PERMISSIONS) == 0) {
        throw new SftpException(SSH_FX_FAILURE, "Can't change directory: " + path);
      }
      if (!attr.isDir()) {
        throw new SftpException(SSH_FX_FAILURE, "Can't change directory: " + path);
      }

      setCwd(Util.byte2str(str, fEncoding));
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void put(String src, String dst) throws SftpException {
    put(src, dst, null, OVERWRITE);
  }

  public void put(String src, String dst, int mode) throws SftpException {
    put(src, dst, null, mode);
  }

  public void put(String src, String dst, SftpProgressMonitor monitor) throws SftpException {
    put(src, dst, monitor, OVERWRITE);
  }

  /**
   * Sends data from <code>src</code> file to <code>dst</code> file. The <code>mode</code> should be
   * <code>OVERWRITE</code>, <code>RESUME</code> or <code>APPEND</code>.
   *
   * @param src source file
   * @param dst destination file
   * @param monitor progress monitor
   * @param mode how data should be added to dst
   */
  public void put(String src, String dst, SftpProgressMonitor monitor, int mode)
      throws SftpException {

    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      src = localAbsolutePath(src);
      dst = remoteAbsolutePath(dst);

      Vector<String> v = glob_remote(dst);
      int vsize = v.size();
      if (vsize != 1) {
        if (vsize == 0) {
          if (isPattern(dst))
            throw new SftpException(SSH_FX_FAILURE, dst);
          else
            dst = Util.unquote(dst);
        }
        throw new SftpException(SSH_FX_FAILURE, v.toString());
      } else {
        dst = v.elementAt(0);
      }

      boolean isRemoteDir = isRemoteDir(dst);

      v = glob_local(src);
      vsize = v.size();

      StringBuilder dstsb = null;
      if (isRemoteDir) {
        if (!dst.endsWith("/")) {
          dst += "/";
        }
        dstsb = new StringBuilder(dst);
      } else if (vsize > 1) {
        throw new SftpException(SSH_FX_FAILURE,
            "Copying multiple files, but the destination is missing or a file.");
      }

      for (int j = 0; j < vsize; j++) {
        String _src = v.elementAt(j);
        String _dst = null;
        if (isRemoteDir) {
          int i = _src.lastIndexOf(file_separatorc);
          if (fs_is_bs) {
            int ii = _src.lastIndexOf('/');
            if (ii != -1 && ii > i)
              i = ii;
          }
          if (i == -1)
            dstsb.append(_src);
          else
            dstsb.append(_src.substring(i + 1));
          _dst = dstsb.toString();
          dstsb.delete(dst.length(), _dst.length());
        } else {
          _dst = dst;
        }
        // System.err.println("_dst "+_dst);

        long size_of_dst = 0;
        if (mode == RESUME) {
          try {
            SftpATTRS attr = _stat(_dst);
            size_of_dst = attr.getSize();
          } catch (Exception eee) {
            // System.err.println(eee);
          }
          long size_of_src = new File(_src).length();
          if (size_of_src < size_of_dst) {
            throw new SftpException(SSH_FX_FAILURE, "failed to resume for " + _dst);
          }
          if (size_of_src == size_of_dst) {
            return;
          }
        }

        if (monitor != null) {
          monitor.init(SftpProgressMonitor.PUT, _src, _dst, (new File(_src)).length());
          if (mode == RESUME) {
            monitor.count(size_of_dst);
          }
        }
        try (InputStream fis = new FileInputStream(_src)) {
          _put(fis, _dst, monitor, mode);
        }
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void put(InputStream src, String dst) throws SftpException {
    put(src, dst, null, OVERWRITE);
  }

  public void put(InputStream src, String dst, int mode) throws SftpException {
    put(src, dst, null, mode);
  }

  public void put(InputStream src, String dst, SftpProgressMonitor monitor) throws SftpException {
    put(src, dst, monitor, OVERWRITE);
  }

  /**
   * Sends data from the input stream <code>src</code> to <code>dst</code> file. The
   * <code>mode</code> should be <code>OVERWRITE</code>, <code>RESUME</code> or <code>APPEND</code>.
   *
   * @param src input stream
   * @param dst destination file
   * @param monitor progress monitor
   * @param mode how data should be added to dst
   */
  public void put(InputStream src, String dst, SftpProgressMonitor monitor, int mode)
      throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      dst = remoteAbsolutePath(dst);

      Vector<String> v = glob_remote(dst);
      int vsize = v.size();
      if (vsize != 1) {
        if (vsize == 0) {
          if (isPattern(dst))
            throw new SftpException(SSH_FX_FAILURE, dst);
          else
            dst = Util.unquote(dst);
        }
        throw new SftpException(SSH_FX_FAILURE, v.toString());
      } else {
        dst = v.elementAt(0);
      }

      if (monitor != null) {
        monitor.init(SftpProgressMonitor.PUT, "-", dst, SftpProgressMonitor.UNKNOWN_SIZE);
      }

      _put(src, dst, monitor, mode);
    } catch (Exception e) {
      if (e instanceof SftpException) {
        if (((SftpException) e).id == SSH_FX_FAILURE && isRemoteDir(dst)) {
          throw new SftpException(SSH_FX_FAILURE, dst + " is a directory");
        }
        throw (SftpException) e;
      }
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void _put(InputStream src, String dst, SftpProgressMonitor monitor, int mode)
      throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      byte[] dstb = Util.str2byte(dst, fEncoding);
      long skip = 0;
      if (mode == RESUME || mode == APPEND) {
        try {
          SftpATTRS attr = _stat(dstb);
          skip = attr.getSize();
        } catch (Exception eee) {
          // System.err.println(eee);
        }
      }
      if (mode == RESUME && skip > 0) {
        long skipped = src.skip(skip);
        if (skipped < skip) {
          throw new SftpException(SSH_FX_FAILURE, "failed to resume for " + dst);
        }
      }

      if (mode == OVERWRITE) {
        sendOPENW(dstb);
      } else {
        sendOPENA(dstb);
      }

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
        throw new SftpException(SSH_FX_FAILURE, "invalid type=" + type);
      }
      if (type == SSH_FXP_STATUS) {
        int i = buf.getInt();
        throwStatusError(buf, i);
      }
      byte[] handle = buf.getString(); // handle
      byte[] data = null;

      boolean dontcopy = true;

      int buffer_margin = session.getBufferMargin();
      if (!dontcopy) { // This case will not work anymore.
        data = new byte[obuf.buffer.length - (5 + 13 + 21 + handle.length + buffer_margin)];
      }

      long offset = 0;
      if (mode == RESUME || mode == APPEND) {
        offset += skip;
      }

      int startid = seq;
      int ackcount = 0;
      int _s = 0;
      int _datalen = 0;

      if (!dontcopy) { // This case will not work anymore.
        _datalen = data.length;
      } else {
        data = obuf.buffer;
        _s = 5 + 13 + 21 + handle.length;
        _datalen = obuf.buffer.length - _s - buffer_margin;
      }

      int bulk_requests = rq.size();

      while (true) {
        int nread = 0;
        int count = 0;
        int s = _s;
        int datalen = _datalen;

        do {
          nread = src.read(data, s, datalen);
          if (nread > 0) {
            s += nread;
            datalen -= nread;
            count += nread;
          }
        } while (datalen > 0 && nread > 0);
        if (count <= 0)
          break;

        int foo = count;
        while (foo > 0) {
          if ((seq - 1) == startid || ((seq - startid) - ackcount) >= bulk_requests) {
            while (((seq - startid) - ackcount) >= bulk_requests) {
              if (checkStatus(ackid, header)) {
                int _ackid = ackid[0];
                if (startid > _ackid || _ackid > seq - 1) {
                  if (_ackid == seq) {
                    if (getSession().getLogger().isEnabled(Logger.ERROR)) {
                      getSession().getLogger().log(Logger.ERROR,
                          "ack error: startid=" + startid + " seq=" + seq + " _ackid=" + _ackid);
                    }
                  } else {
                    throw new SftpException(SSH_FX_FAILURE,
                        "ack error: startid=" + startid + " seq=" + seq + " _ackid=" + _ackid);
                  }
                }
                ackcount++;
              } else {
                break;
              }
            }
          }
          if (dontcopy) {
            foo -= sendWRITE(handle, offset, data, 0, foo);
            if (data != obuf.buffer) {
              data = obuf.buffer;
              _datalen = obuf.buffer.length - _s - buffer_margin;
            }
          } else {
            foo -= sendWRITE(handle, offset, data, _s, foo);
          }
        }
        offset += count;
        if (monitor != null && !monitor.count(count)) {
          break;
        }
      }
      int _ackcount = seq - startid;
      while (_ackcount > ackcount) {
        if (!checkStatus(null, header)) {
          break;
        }
        ackcount++;
      }
      if (monitor != null)
        monitor.end();
      _sendCLOSE(handle, header);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public OutputStream put(String dst) throws SftpException {
    return put(dst, (SftpProgressMonitor) null, OVERWRITE);
  }

  public OutputStream put(String dst, final int mode) throws SftpException {
    return put(dst, (SftpProgressMonitor) null, mode);
  }

  public OutputStream put(String dst, final SftpProgressMonitor monitor, final int mode)
      throws SftpException {
    return put(dst, monitor, mode, 0);
  }

  /**
   * Sends data from the output stream to <code>dst</code> file. The <code>mode</code> should be
   * <code>OVERWRITE</code>, <code>RESUME</code> or <code>APPEND</code>.
   *
   * @param dst destination file
   * @param monitor progress monitor
   * @param mode how data should be added to dst
   * @param offset data will be added at offset
   * @return output stream, which accepts data to be transferred.
   */
  public OutputStream put(String dst, final SftpProgressMonitor monitor, final int mode,
      long offset) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      dst = remoteAbsolutePath(dst);
      dst = isUnique(dst);

      if (isRemoteDir(dst)) {
        throw new SftpException(SSH_FX_FAILURE, dst + " is a directory");
      }

      byte[] dstb = Util.str2byte(dst, fEncoding);

      long skip = 0;
      if (mode == RESUME || mode == APPEND) {
        try {
          SftpATTRS attr = _stat(dstb);
          skip = attr.getSize();
        } catch (Exception eee) {
          // System.err.println(eee);
        }
      }

      if (monitor != null) {
        monitor.init(SftpProgressMonitor.PUT, "-", dst, SftpProgressMonitor.UNKNOWN_SIZE);
      }

      if (mode == OVERWRITE) {
        sendOPENW(dstb);
      } else {
        sendOPENA(dstb);
      }

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      if (type == SSH_FXP_STATUS) {
        int i = buf.getInt();
        throwStatusError(buf, i);
      }
      final byte[] handle = buf.getString(); // handle

      if (mode == RESUME || mode == APPEND) {
        offset += skip;
      }

      final long[] _offset = new long[1];
      _offset[0] = offset;
      OutputStream out = new OutputStream() {
        private boolean init = true;
        private boolean isClosed = false;
        private int[] ackid = new int[1];
        private int startid = 0;
        private int _ackid = 0;
        private int ackcount = 0;
        private int writecount = 0;
        private Header header = new Header();

        @Override
        public void write(byte[] d) throws IOException {
          write(d, 0, d.length);
        }

        @Override
        public void write(byte[] d, int s, int len) throws IOException {
          if (init) {
            startid = seq;
            _ackid = seq;
            init = false;
          }

          if (isClosed) {
            throw new IOException("stream already closed");
          }

          try {
            int _len = len;
            while (_len > 0) {
              if (useWriteFlushWorkaround && rwsize < 21 + handle.length + _len + 4) {
                flush();
              }
              int sent = sendWRITE(handle, _offset[0], d, s, _len);
              writecount++;
              _offset[0] += sent;
              s += sent;
              _len -= sent;
              if ((seq - 1) == startid || io_in.available() >= 1024) {
                while (io_in.available() > 0) {
                  if (checkStatus(ackid, header)) {
                    _ackid = ackid[0];
                    if (startid > _ackid || _ackid > seq - 1) {
                      throw new SftpException(SSH_FX_FAILURE, "");
                    }
                    ackcount++;
                  } else {
                    break;
                  }
                }
              }
            }
            if (monitor != null && !monitor.count(len)) {
              close();
              throw new IOException("canceled");
            }
          } catch (IOException e) {
            throw e;
          } catch (Exception e) {
            throw new IOException(e.toString(), e);
          }
        }

        byte[] _data = new byte[1];

        @Override
        public void write(int foo) throws IOException {
          _data[0] = (byte) foo;
          write(_data, 0, 1);
        }

        @Override
        public void flush() throws IOException {

          if (isClosed) {
            throw new IOException("stream already closed");
          }

          if (!init) {
            try {
              while (writecount > ackcount) {
                if (!checkStatus(null, header)) {
                  break;
                }
                ackcount++;
              }
            } catch (SftpException e) {
              throw new IOException(e.toString(), e);
            }
          }
        }

        @Override
        public void close() throws IOException {
          if (isClosed) {
            return;
          }
          flush();
          if (monitor != null)
            monitor.end();
          try {
            _sendCLOSE(handle, header);
          } catch (IOException e) {
            throw e;
          } catch (Exception e) {
            throw new IOException(e.toString(), e);
          }
          isClosed = true;
        }
      };
      return out;
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void get(String src, String dst) throws SftpException {
    get(src, dst, null, OVERWRITE);
  }

  public void get(String src, String dst, SftpProgressMonitor monitor) throws SftpException {
    get(src, dst, monitor, OVERWRITE);
  }

  public void get(String src, String dst, SftpProgressMonitor monitor, int mode)
      throws SftpException {
    // System.out.println("get: "+src+" "+dst);

    boolean _dstExist = false;
    String _dst = null;
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      src = remoteAbsolutePath(src);
      dst = localAbsolutePath(dst);

      Vector<String> v = glob_remote(src);
      int vsize = v.size();
      if (vsize == 0) {
        throw new SftpException(SSH_FX_NO_SUCH_FILE, "No such file");
      }

      File dstFile = new File(dst);
      boolean isDstDir = dstFile.isDirectory();
      StringBuilder dstsb = null;
      if (isDstDir) {
        if (!dst.endsWith(file_separator)) {
          dst += file_separator;
        }
        dstsb = new StringBuilder(dst);
      } else if (vsize > 1) {
        throw new SftpException(SSH_FX_FAILURE,
            "Copying multiple files, but destination is missing or a file.");
      }

      for (int j = 0; j < vsize; j++) {
        String _src = v.elementAt(j);
        SftpATTRS attr = _stat(_src);
        if (attr.isDir()) {
          throw new SftpException(SSH_FX_FAILURE, "not supported to get directory " + _src);
        }

        _dst = null;
        if (isDstDir) {
          int i = _src.lastIndexOf('/');
          if (i == -1)
            dstsb.append(_src);
          else
            dstsb.append(_src.substring(i + 1));
          _dst = dstsb.toString();
          if (_dst.indexOf("..") != -1) {
            String dstc = (new File(dst)).getCanonicalPath();
            String _dstc = (new File(_dst)).getCanonicalPath();
            if (!(_dstc.length() > dstc.length()
                && _dstc.substring(0, dstc.length() + 1).equals(dstc + file_separator))) {
              throw new SftpException(SSH_FX_FAILURE, "writing to an unexpected file " + _src);
            }
          }
          dstsb.delete(dst.length(), _dst.length());
        } else {
          _dst = dst;
        }

        File _dstFile = new File(_dst);
        if (mode == RESUME) {
          long size_of_src = attr.getSize();
          long size_of_dst = _dstFile.length();
          if (size_of_dst > size_of_src) {
            throw new SftpException(SSH_FX_FAILURE, "failed to resume for " + _dst);
          }
          if (size_of_dst == size_of_src) {
            return;
          }
        }

        if (monitor != null) {
          monitor.init(SftpProgressMonitor.GET, _src, _dst, attr.getSize());
          if (mode == RESUME) {
            monitor.count(_dstFile.length());
          }
        }

        _dstExist = _dstFile.exists();
        try (OutputStream fos = mode == OVERWRITE ? new FileOutputStream(_dst)
            : new FileOutputStream(_dst, true) /* append */) {
          // System.err.println("_get: "+_src+", "+_dst);
          _get(_src, fos, monitor, mode, new File(_dst).length());
        }
      }
    } catch (Exception e) {
      if (!_dstExist && _dst != null) {
        File _dstFile = new File(_dst);
        if (_dstFile.exists() && _dstFile.length() == 0) {
          _dstFile.delete();
        }
      }
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void get(String src, OutputStream dst) throws SftpException {
    get(src, dst, null, OVERWRITE, 0);
  }

  public void get(String src, OutputStream dst, SftpProgressMonitor monitor) throws SftpException {
    get(src, dst, monitor, OVERWRITE, 0);
  }

  public void get(String src, OutputStream dst, SftpProgressMonitor monitor, int mode, long skip)
      throws SftpException {
    // System.err.println("get: "+src+", "+dst);
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      src = remoteAbsolutePath(src);
      src = isUnique(src);

      if (monitor != null) {
        SftpATTRS attr = _stat(src);
        monitor.init(SftpProgressMonitor.GET, src, "??", attr.getSize());
        if (mode == RESUME) {
          monitor.count(skip);
        }
      }
      _get(src, dst, monitor, mode, skip);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  private void _get(String src, OutputStream dst, SftpProgressMonitor monitor, int mode, long skip)
      throws SftpException {
    // System.err.println("_get: "+src+", "+dst);

    byte[] srcb = Util.str2byte(src, fEncoding);
    try {
      sendOPENR(srcb);

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }

      if (type == SSH_FXP_STATUS) {
        int i = buf.getInt();
        throwStatusError(buf, i);
      }

      byte[] handle = buf.getString(); // filename

      long offset = 0;
      if (mode == RESUME) {
        offset += skip;
      }

      int request_max = 1;
      rq.init();
      long request_offset = offset;

      int request_len = buf.buffer.length - 13;
      if (server_version == 0) {
        request_len = 1024;
      }

      loop: while (true) {

        while (rq.count() < request_max) {
          sendREAD(handle, request_offset, request_len, rq);
          request_offset += request_len;
        }

        header = header(buf, header);
        length = header.length;
        type = header.type;

        RequestQueue.Request rr = null;
        try {
          rr = rq.get(header.rid);
        } catch (RequestQueue.OutOfOrderException e) {
          request_offset = e.offset;
          skip(header.length);
          rq.cancel(header, buf);
          continue;
        }

        if (type == SSH_FXP_STATUS) {
          fill(buf, length);
          int i = buf.getInt();
          if (i == SSH_FX_EOF) {
            break loop;
          }
          throwStatusError(buf, i);
        }

        if (type != SSH_FXP_DATA) {
          break loop;
        }

        buf.rewind();
        fill(buf.buffer, 0, 4);
        length -= 4;
        int length_of_data = buf.getInt(); // length of data

        /*
         * Since sftp protocol version 6, "end-of-file" has been defined, byte SSH_FXP_DATA uint32
         * request-id string data bool end-of-file [optional] but some sftpd server will send such a
         * field in the sftp protocol 3 ;-(
         */
        int optional_data = length - length_of_data;

        int foo = length_of_data;
        while (foo > 0) {
          int bar = foo;
          if (bar > buf.buffer.length) {
            bar = buf.buffer.length;
          }
          int data_len = io_in.read(buf.buffer, 0, bar);
          if (data_len < 0) {
            break loop;
          }

          dst.write(buf.buffer, 0, data_len);

          offset += data_len;
          foo -= data_len;

          if (monitor != null) {
            if (!monitor.count(data_len)) {
              skip(foo);
              if (optional_data > 0) {
                skip(optional_data);
              }
              break loop;
            }
          }
        }
        // System.err.println("length: "+length); // length should be 0

        if (optional_data > 0) {
          skip(optional_data);
        }

        if (length_of_data < rr.length) { //
          rq.cancel(header, buf);
          sendREAD(handle, rr.offset + length_of_data, (int) (rr.length - length_of_data), rq);
          request_offset = rr.offset + rr.length;
        }

        if (request_max < rq.size()) {
          request_max++;
        }
      }
      dst.flush();

      if (monitor != null)
        monitor.end();

      rq.cancel(header, buf);

      _sendCLOSE(handle, header);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  private class RequestQueue {
    class OutOfOrderException extends Exception {
      private static final long serialVersionUID = -1L;
      long offset;

      OutOfOrderException(long offset) {
        this.offset = offset;
      }
    }

    class Request {
      int id;
      long offset;
      long length;
    }

    Request[] rrq = null;
    int head, count;

    RequestQueue(int size) {
      rrq = new Request[size];
      for (int i = 0; i < rrq.length; i++) {
        rrq[i] = new Request();
      }
      init();
    }

    void init() {
      head = count = 0;
    }

    void add(int id, long offset, int length) {
      if (count == 0)
        head = 0;
      int tail = head + count;
      if (tail >= rrq.length)
        tail -= rrq.length;
      rrq[tail].id = id;
      rrq[tail].offset = offset;
      rrq[tail].length = length;
      count++;
    }

    Request get(int id) throws OutOfOrderException, SftpException {
      count -= 1;
      int i = head;
      head++;
      if (head == rrq.length)
        head = 0;
      if (rrq[i].id != id) {
        long offset = getOffset();
        boolean find = false;
        for (int j = 0; j < rrq.length; j++) {
          if (rrq[j].id == id) {
            find = true;
            rrq[j].id = 0;
            break;
          }
        }
        if (find)
          throw new OutOfOrderException(offset);
        throw new SftpException(SSH_FX_FAILURE, "RequestQueue: unknown request id " + id);
      }
      rrq[i].id = 0;
      return rrq[i];
    }

    int count() {
      return count;
    }

    int size() {
      return rrq.length;
    }

    void cancel(Header header, Buffer buf) throws IOException {
      int _count = count;
      for (int i = 0; i < _count; i++) {
        header = header(buf, header);
        int length = header.length;
        for (int j = 0; j < rrq.length; j++) {
          if (rrq[j].id == header.rid) {
            rrq[j].id = 0;
            break;
          }
        }
        skip(length);
      }
      init();
    }

    long getOffset() {
      long result = Long.MAX_VALUE;

      for (int i = 0; i < rrq.length; i++) {
        if (rrq[i].id == 0)
          continue;
        if (result > rrq[i].offset)
          result = rrq[i].offset;
      }

      return result;
    }
  }

  public InputStream get(String src) throws SftpException {
    return get(src, null, 0L);
  }

  public InputStream get(String src, SftpProgressMonitor monitor) throws SftpException {
    return get(src, monitor, 0L);
  }

  /**
   * @deprecated This method will be deleted in the future.
   */
  @Deprecated
  public InputStream get(String src, int mode) throws SftpException {
    return get(src, null, 0L);
  }

  /**
   * @deprecated This method will be deleted in the future.
   */
  @Deprecated
  public InputStream get(String src, final SftpProgressMonitor monitor, final int mode)
      throws SftpException {
    return get(src, monitor, 0L);
  }

  public InputStream get(String src, final SftpProgressMonitor monitor, final long skip)
      throws SftpException {

    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      src = remoteAbsolutePath(src);
      src = isUnique(src);

      byte[] srcb = Util.str2byte(src, fEncoding);

      SftpATTRS attr = _stat(srcb);
      if (monitor != null) {
        monitor.init(SftpProgressMonitor.GET, src, "??", attr.getSize());
      }

      sendOPENR(srcb);

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      if (type == SSH_FXP_STATUS) {
        int i = buf.getInt();
        throwStatusError(buf, i);
      }

      final byte[] handle = buf.getString(); // handle

      rq.init();

      InputStream in = new InputStream() {
        long offset = skip;
        boolean closed = false;
        int rest_length = 0;
        byte[] _data = new byte[1];
        byte[] rest_byte = new byte[1024];
        Header header = new Header();
        int request_max = 1;
        long request_offset = offset;

        @Override
        public int read() throws IOException {
          if (closed)
            return -1;
          int i = read(_data, 0, 1);
          if (i == -1) {
            return -1;
          } else {
            return _data[0] & 0xff;
          }
        }

        @Override
        public int read(byte[] d) throws IOException {
          if (closed)
            return -1;
          return read(d, 0, d.length);
        }

        @Override
        public int read(byte[] d, int s, int len) throws IOException {
          if (closed)
            return -1;
          if (d == null) {
            throw new NullPointerException();
          }
          if (s < 0 || len < 0 || s + len > d.length) {
            throw new IndexOutOfBoundsException();
          }
          if (len == 0) {
            return 0;
          }

          if (rest_length > 0) {
            int foo = rest_length;
            if (foo > len)
              foo = len;
            System.arraycopy(rest_byte, 0, d, s, foo);
            if (foo != rest_length) {
              System.arraycopy(rest_byte, foo, rest_byte, 0, rest_length - foo);
            }

            if (monitor != null) {
              if (!monitor.count(foo)) {
                close();
                return -1;
              }
            }

            rest_length -= foo;
            return foo;
          }

          if (buf.buffer.length - 13 < len) {
            len = buf.buffer.length - 13;
          }
          if (server_version == 0 && len > 1024) {
            len = 1024;
          }

          if (rq.count() == 0 || true // working around slow transfer speed for
                                      // some sftp servers including Titan FTP.
          ) {
            int request_len = buf.buffer.length - 13;
            if (server_version == 0) {
              request_len = 1024;
            }

            while (rq.count() < request_max) {
              try {
                sendREAD(handle, request_offset, request_len, rq);
              } catch (Exception e) {
                throw new IOException("error");
              }
              request_offset += request_len;
            }
          }

          header = header(buf, header);
          rest_length = header.length;
          int type = header.type;
          int id = header.rid;

          RequestQueue.Request rr = null;
          try {
            rr = rq.get(header.rid);
          } catch (RequestQueue.OutOfOrderException e) {
            request_offset = e.offset;
            skip(header.length);
            rq.cancel(header, buf);
            return 0;
          } catch (SftpException e) {
            throw new IOException("error: " + e.toString(), e);
          }

          if (type != SSH_FXP_STATUS && type != SSH_FXP_DATA) {
            throw new IOException("error");
          }
          if (type == SSH_FXP_STATUS) {
            fill(buf, rest_length);
            int i = buf.getInt();
            rest_length = 0;
            if (i == SSH_FX_EOF) {
              close();
              return -1;
            }
            // throwStatusError(buf, i);
            throw new IOException("error");
          }

          buf.rewind();
          fill(buf.buffer, 0, 4);
          int length_of_data = buf.getInt();
          rest_length -= 4;

          /*
           * Since sftp protocol version 6, "end-of-file" has been defined, byte SSH_FXP_DATA uint32
           * request-id string data bool end-of-file [optional] but some sftpd server will send such
           * a field in the sftp protocol 3 ;-(
           */
          int optional_data = rest_length - length_of_data;

          offset += length_of_data;
          int foo = length_of_data;
          if (foo > 0) {
            int bar = foo;
            if (bar > len) {
              bar = len;
            }
            int i = io_in.read(d, s, bar);
            if (i < 0) {
              return -1;
            }
            foo -= i;
            rest_length = foo;

            if (foo > 0) {
              if (rest_byte.length < foo) {
                rest_byte = new byte[foo];
              }
              int _s = 0;
              int _len = foo;
              int j;
              while (_len > 0) {
                j = io_in.read(rest_byte, _s, _len);
                if (j <= 0)
                  break;
                _s += j;
                _len -= j;
              }
            }

            if (optional_data > 0) {
              io_in.skip(optional_data);
            }

            if (length_of_data < rr.length) { //
              rq.cancel(header, buf);
              try {
                sendREAD(handle, rr.offset + length_of_data, (int) (rr.length - length_of_data),
                    rq);
              } catch (Exception e) {
                throw new IOException("error");
              }
              request_offset = rr.offset + rr.length;
            }

            if (request_max < rq.size()) {
              request_max++;
            }

            if (monitor != null) {
              if (!monitor.count(i)) {
                close();
                return -1;
              }
            }

            return i;
          }
          return 0; // ??
        }

        @Override
        public void close() throws IOException {
          if (closed)
            return;
          closed = true;
          if (monitor != null)
            monitor.end();
          rq.cancel(header, buf);
          try {
            _sendCLOSE(handle, header);
          } catch (IOException e) {
            throw e;
          } catch (Exception e) {
            throw new IOException(e.toString(), e);
          }
        }
      };
      return in;
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public Vector<LsEntry> ls(String path) throws SftpException {
    final Vector<LsEntry> v = new Vector<>();
    LsEntrySelector selector = new LsEntrySelector() {
      @Override
      public int select(LsEntry entry) {
        v.addElement(entry);
        return CONTINUE;
      }
    };
    ls(path, selector);
    return v;
  }

  /**
   * List files specified by the remote <code>path</code>. Each files and directories will be passed
   * to <code>LsEntrySelector#select(LsEntry)</code> method, and if that method returns
   * <code>LsEntrySelector#BREAK</code>, the operation will be canceled immediately.
   *
   * @see ChannelSftp.LsEntrySelector
   * @since 0.1.47
   */
  public void ls(String path, LsEntrySelector selector) throws SftpException {
    // System.out.println("ls: "+path);
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);
      byte[] pattern = null;
      Vector<LsEntry> v = new Vector<>();

      int foo = path.lastIndexOf('/');
      String dir = path.substring(0, ((foo == 0) ? 1 : foo));
      String _pattern = path.substring(foo + 1);
      dir = Util.unquote(dir);

      // If pattern has included '*' or '?', we need to convert
      // to UTF-8 string before globbing.
      byte[][] _pattern_utf8 = new byte[1][];
      boolean pattern_has_wildcard = isPattern(_pattern, _pattern_utf8);

      if (pattern_has_wildcard) {
        pattern = _pattern_utf8[0];
      } else {
        String upath = Util.unquote(path);
        // SftpATTRS attr=_lstat(upath);
        SftpATTRS attr = _stat(upath);
        if (attr.isDir()) {
          pattern = null;
          dir = upath;
        } else {
          /*
           * // If we can generage longname by ourself, // we don't have to use openDIR. String
           * filename=Util.unquote(_pattern); String longname=... v.addElement(new LsEntry(filename,
           * longname, attr)); return v;
           */

          if (fEncoding_is_utf8) {
            pattern = _pattern_utf8[0];
            pattern = Util.unquote(pattern);
          } else {
            _pattern = Util.unquote(_pattern);
            pattern = Util.str2byte(_pattern, fEncoding);
          }
        }
      }

      sendOPENDIR(Util.str2byte(dir, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      if (type == SSH_FXP_STATUS) {
        int i = buf.getInt();
        throwStatusError(buf, i);
      }

      int cancel = LsEntrySelector.CONTINUE;
      byte[] handle = buf.getString(); // handle

      while (cancel == LsEntrySelector.CONTINUE) {

        sendREADDIR(handle);

        header = header(buf, header);
        length = header.length;
        type = header.type;
        if (type != SSH_FXP_STATUS && type != SSH_FXP_NAME) {
          throw new SftpException(SSH_FX_FAILURE, "");
        }
        if (type == SSH_FXP_STATUS) {
          fill(buf, length);
          int i = buf.getInt();
          if (i == SSH_FX_EOF)
            break;
          throwStatusError(buf, i);
        }

        buf.rewind();
        fill(buf.buffer, 0, 4);
        length -= 4;
        int count = buf.getInt();

        byte[] str;
        int flags;

        buf.reset();
        while (count > 0) {
          if (length > 0) {
            buf.shift();
            int j = (buf.buffer.length > (buf.index + length)) ? length
                : (buf.buffer.length - buf.index);
            int i = fill(buf.buffer, buf.index, j);
            buf.index += i;
            length -= i;
          }
          byte[] filename = buf.getString();
          byte[] longname = null;
          if (server_version <= 3) {
            longname = buf.getString();
          }
          SftpATTRS attrs = SftpATTRS.getATTR(buf);

          if (cancel == LsEntrySelector.BREAK) {
            count--;
            continue;
          }

          boolean find = false;
          String f = null;
          if (pattern == null) {
            find = true;
          } else if (!pattern_has_wildcard) {
            find = Util.array_equals(pattern, filename);
          } else {
            byte[] _filename = filename;
            if (!fEncoding_is_utf8) {
              f = Util.byte2str(_filename, fEncoding);
              _filename = Util.str2byte(f, StandardCharsets.UTF_8);
            }
            find = Util.glob(pattern, _filename);
          }

          if (find) {
            if (f == null) {
              f = Util.byte2str(filename, fEncoding);
            }
            String l = null;
            if (longname == null) {
              // TODO: we need to generate long name from attrs
              // for the sftp protocol 4(and later).
              l = attrs.toString() + " " + f;
            } else {
              l = Util.byte2str(longname, fEncoding);
            }

            cancel = selector.select(new LsEntry(f, l, attrs));
          }

          count--;
        }
      }
      _sendCLOSE(handle, header);

      /*
       * if(v.size()==1 && pattern_has_wildcard){ LsEntry le=(LsEntry)v.elementAt(0);
       * if(le.getAttrs().isDir()){ String f=le.getFilename(); if(isPattern(f)){ f=Util.quote(f); }
       * if(!dir.endsWith("/")){ dir+="/"; } v=null; return ls(dir+f); } }
       */

    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public String readlink(String path) throws SftpException {
    try {
      if (server_version < 3) {
        throw new SftpException(SSH_FX_OP_UNSUPPORTED,
            "The remote sshd is too old to support symlink operation.");
      }

      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      path = isUnique(path);

      sendREADLINK(Util.str2byte(path, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS && type != SSH_FXP_NAME) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      if (type == SSH_FXP_NAME) {
        int count = buf.getInt(); // count
        byte[] filename = null;
        for (int i = 0; i < count; i++) {
          filename = buf.getString();
          if (server_version <= 3) {
            byte[] longname = buf.getString();
          }
          SftpATTRS.getATTR(buf);
        }
        return Util.byte2str(filename, fEncoding);
      }

      int i = buf.getInt();
      throwStatusError(buf, i);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
    return null;
  }

  public void symlink(String oldpath, String newpath) throws SftpException {
    if (server_version < 3) {
      throw new SftpException(SSH_FX_OP_UNSUPPORTED,
          "The remote sshd is too old to support symlink operation.");
    }

    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      String _oldpath = remoteAbsolutePath(oldpath);
      newpath = remoteAbsolutePath(newpath);

      _oldpath = isUnique(_oldpath);
      if (oldpath.charAt(0) != '/') { // relative path
        String cwd = getCwd();
        oldpath = _oldpath.substring(cwd.length() + (cwd.endsWith("/") ? 0 : 1));
      } else {
        oldpath = _oldpath;
      }

      if (isPattern(newpath)) {
        throw new SftpException(SSH_FX_FAILURE, newpath);
      }
      newpath = Util.unquote(newpath);

      sendSYMLINK(Util.str2byte(oldpath, fEncoding), Util.str2byte(newpath, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }

      int i = buf.getInt();
      if (i == SSH_FX_OK)
        return;
      throwStatusError(buf, i);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void hardlink(String oldpath, String newpath) throws SftpException {
    if (!extension_hardlink) {
      throw new SftpException(SSH_FX_OP_UNSUPPORTED, "hardlink@openssh.com is not supported");
    }

    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      String _oldpath = remoteAbsolutePath(oldpath);
      newpath = remoteAbsolutePath(newpath);

      _oldpath = isUnique(_oldpath);
      if (oldpath.charAt(0) != '/') { // relative path
        String cwd = getCwd();
        oldpath = _oldpath.substring(cwd.length() + (cwd.endsWith("/") ? 0 : 1));
      } else {
        oldpath = _oldpath;
      }

      if (isPattern(newpath)) {
        throw new SftpException(SSH_FX_FAILURE, newpath);
      }
      newpath = Util.unquote(newpath);

      sendHARDLINK(Util.str2byte(oldpath, fEncoding), Util.str2byte(newpath, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }

      int i = buf.getInt();
      if (i == SSH_FX_OK)
        return;
      throwStatusError(buf, i);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void rename(String oldpath, String newpath) throws SftpException {
    if (server_version < 2) {
      throw new SftpException(SSH_FX_OP_UNSUPPORTED,
          "The remote sshd is too old to support rename operation.");
    }

    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      oldpath = remoteAbsolutePath(oldpath);
      newpath = remoteAbsolutePath(newpath);

      oldpath = isUnique(oldpath);

      Vector<String> v = glob_remote(newpath);
      int vsize = v.size();
      if (vsize >= 2) {
        throw new SftpException(SSH_FX_FAILURE, v.toString());
      }
      if (vsize == 1) {
        newpath = v.elementAt(0);
      } else { // vsize==0
        if (isPattern(newpath))
          throw new SftpException(SSH_FX_FAILURE, newpath);
        newpath = Util.unquote(newpath);
      }

      sendRENAME(Util.str2byte(oldpath, fEncoding), Util.str2byte(newpath, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }

      int i = buf.getInt();
      if (i == SSH_FX_OK)
        return;
      throwStatusError(buf, i);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void rm(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();

      Header header = new Header();

      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);
        sendREMOVE(Util.str2byte(path, fEncoding));

        header = header(buf, header);
        int length = header.length;
        int type = header.type;

        fill(buf, length);

        if (type != SSH_FXP_STATUS) {
          throw new SftpException(SSH_FX_FAILURE, "");
        }
        int i = buf.getInt();
        if (i != SSH_FX_OK) {
          throwStatusError(buf, i);
        }
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  private boolean isRemoteDir(String path) {
    try {
      sendSTAT(Util.str2byte(path, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_ATTRS) {
        return false;
      }
      SftpATTRS attr = SftpATTRS.getATTR(buf);
      return attr.isDir();
    } catch (Exception e) {
    }
    return false;
  }

  public void chgrp(int gid, String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();
      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);

        SftpATTRS attr = _stat(path);

        attr.setFLAGS(0);
        attr.setUIDGID(attr.uid, gid);
        _setStat(path, attr);
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void chown(int uid, String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();
      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);

        SftpATTRS attr = _stat(path);

        attr.setFLAGS(0);
        attr.setUIDGID(uid, attr.gid);
        _setStat(path, attr);
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void chmod(int permissions, String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();
      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);

        SftpATTRS attr = _stat(path);

        attr.setFLAGS(0);
        attr.setPERMISSIONS(permissions);
        _setStat(path, attr);
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void setMtime(String path, int mtime) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();
      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);

        SftpATTRS attr = _stat(path);

        attr.setFLAGS(0);
        attr.setACMODTIME(attr.getATime(), mtime);
        _setStat(path, attr);
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void rmdir(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();

      Header header = new Header();

      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);
        sendRMDIR(Util.str2byte(path, fEncoding));

        header = header(buf, header);
        int length = header.length;
        int type = header.type;

        fill(buf, length);

        if (type != SSH_FXP_STATUS) {
          throw new SftpException(SSH_FX_FAILURE, "");
        }

        int i = buf.getInt();
        if (i != SSH_FX_OK) {
          throwStatusError(buf, i);
        }
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void mkdir(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      sendMKDIR(Util.str2byte(path, fEncoding), null);

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }

      int i = buf.getInt();
      if (i == SSH_FX_OK)
        return;
      throwStatusError(buf, i);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public SftpATTRS stat(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);
      path = isUnique(path);

      return _stat(path);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
    // return null;
  }

  private SftpATTRS _stat(byte[] path) throws SftpException {
    try {

      sendSTAT(path);

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_ATTRS) {
        if (type == SSH_FXP_STATUS) {
          int i = buf.getInt();
          throwStatusError(buf, i);
        }
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      SftpATTRS attr = SftpATTRS.getATTR(buf);
      return attr;
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
    // return null;
  }

  private SftpATTRS _stat(String path) throws SftpException {
    return _stat(Util.str2byte(path, fEncoding));
  }

  public SftpStatVFS statVFS(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);
      path = isUnique(path);

      return _statVFS(path);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
    // return null;
  }

  private SftpStatVFS _statVFS(byte[] path) throws SftpException {
    if (!extension_statvfs) {
      throw new SftpException(SSH_FX_OP_UNSUPPORTED, "statvfs@openssh.com is not supported");
    }

    try {

      sendSTATVFS(path);

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != (SSH_FXP_EXTENDED_REPLY & 0xff)) {
        if (type == SSH_FXP_STATUS) {
          int i = buf.getInt();
          throwStatusError(buf, i);
        }
        throw new SftpException(SSH_FX_FAILURE, "");
      } else {
        SftpStatVFS stat = SftpStatVFS.getStatVFS(buf);
        return stat;
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
    // return null;
  }

  private SftpStatVFS _statVFS(String path) throws SftpException {
    return _statVFS(Util.str2byte(path, fEncoding));
  }

  public SftpATTRS lstat(String path) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);
      path = isUnique(path);

      return _lstat(path);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  private SftpATTRS _lstat(String path) throws SftpException {
    try {
      sendLSTAT(Util.str2byte(path, fEncoding));

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_ATTRS) {
        if (type == SSH_FXP_STATUS) {
          int i = buf.getInt();
          throwStatusError(buf, i);
        }
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      SftpATTRS attr = SftpATTRS.getATTR(buf);
      return attr;
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  private byte[] _realpath(String path) throws SftpException, IOException, Exception {
    sendREALPATH(Util.str2byte(path, fEncoding));

    Header header = new Header();
    header = header(buf, header);
    int length = header.length;
    int type = header.type;

    fill(buf, length);

    if (type != SSH_FXP_STATUS && type != SSH_FXP_NAME) {
      throw new SftpException(SSH_FX_FAILURE, "");
    }
    int i;
    if (type == SSH_FXP_STATUS) {
      i = buf.getInt();
      throwStatusError(buf, i);
    }
    i = buf.getInt(); // count

    byte[] str = null;
    while (i-- > 0) {
      str = buf.getString(); // absolute path;
      if (server_version <= 3) {
        byte[] lname = buf.getString(); // long filename
      }
      SftpATTRS attr = SftpATTRS.getATTR(buf); // dummy attribute
    }
    return str;
  }

  public void setStat(String path, SftpATTRS attr) throws SftpException {
    try {
      ((MyPipedInputStream) io_in).updateReadSide();

      path = remoteAbsolutePath(path);

      Vector<String> v = glob_remote(path);
      int vsize = v.size();
      for (int j = 0; j < vsize; j++) {
        path = v.elementAt(j);
        _setStat(path, attr);
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  private void _setStat(String path, SftpATTRS attr) throws SftpException {
    try {
      sendSETSTAT(Util.str2byte(path, fEncoding), attr);

      Header header = new Header();
      header = header(buf, header);
      int length = header.length;
      int type = header.type;

      fill(buf, length);

      if (type != SSH_FXP_STATUS) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      int i = buf.getInt();
      if (i != SSH_FX_OK) {
        throwStatusError(buf, i);
      }
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public String pwd() throws SftpException {
    return getCwd();
  }

  public String lpwd() {
    return lcwd;
  }

  public String version() {
    return version;
  }

  public String getHome() throws SftpException {
    if (home == null) {
      try {
        ((MyPipedInputStream) io_in).updateReadSide();

        byte[] _home = _realpath("");
        home = Util.byte2str(_home, fEncoding);
      } catch (Exception e) {
        if (e instanceof SftpException)
          throw (SftpException) e;
        throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
      }
    }
    return home;
  }

  private String getCwd() throws SftpException {
    if (cwd == null)
      cwd = getHome();
    return cwd;
  }

  private void setCwd(String cwd) {
    this.cwd = cwd;
  }

  private void read(byte[] buf, int s, int l) throws IOException, SftpException {
    int i = 0;
    while (l > 0) {
      i = io_in.read(buf, s, l);
      if (i <= 0) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      s += i;
      l -= i;
    }
  }

  private boolean checkStatus(int[] ackid, Header header) throws IOException, SftpException {
    header = header(buf, header);
    int length = header.length;
    int type = header.type;
    if (ackid != null)
      ackid[0] = header.rid;

    fill(buf, length);

    if (type != SSH_FXP_STATUS) {
      throw new SftpException(SSH_FX_FAILURE, "");
    }
    int i = buf.getInt();
    if (i != SSH_FX_OK) {
      throwStatusError(buf, i);
    }
    return true;
  }

  private boolean _sendCLOSE(byte[] handle, Header header) throws Exception {
    sendCLOSE(handle);
    return checkStatus(null, header);
  }

  private void sendINIT() throws Exception {
    packet.reset();
    putHEAD(SSH_FXP_INIT, 5);
    buf.putInt(3); // version 3
    getSession().write(packet, this, 5 + 4);
  }

  private void sendREALPATH(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_REALPATH, path);
  }

  private void sendSTAT(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_STAT, path);
  }

  private void sendSTATVFS(byte[] path) throws Exception {
    sendPacketPath((byte) 0, path, "statvfs@openssh.com");
  }

  /*
   * private void sendFSTATVFS(byte[] handle) throws Exception{ sendPacketPath((byte)0, handle,
   * "fstatvfs@openssh.com"); }
   */
  private void sendLSTAT(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_LSTAT, path);
  }

  private void sendFSTAT(byte[] handle) throws Exception {
    sendPacketPath(SSH_FXP_FSTAT, handle);
  }

  private void sendSETSTAT(byte[] path, SftpATTRS attr) throws Exception {
    packet.reset();
    putHEAD(SSH_FXP_SETSTAT, 9 + path.length + attr.length());
    buf.putInt(seq++);
    buf.putString(path); // path
    attr.dump(buf);
    getSession().write(packet, this, 9 + path.length + attr.length() + 4);
  }

  private void sendREMOVE(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_REMOVE, path);
  }

  private void sendMKDIR(byte[] path, SftpATTRS attr) throws Exception {
    packet.reset();
    putHEAD(SSH_FXP_MKDIR, 9 + path.length + (attr != null ? attr.length() : 4));
    buf.putInt(seq++);
    buf.putString(path); // path
    if (attr != null)
      attr.dump(buf);
    else
      buf.putInt(0);
    getSession().write(packet, this, 9 + path.length + (attr != null ? attr.length() : 4) + 4);
  }

  private void sendRMDIR(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_RMDIR, path);
  }

  private void sendSYMLINK(byte[] p1, byte[] p2) throws Exception {
    sendPacketPath(SSH_FXP_SYMLINK, p1, p2);
  }

  private void sendHARDLINK(byte[] p1, byte[] p2) throws Exception {
    sendPacketPath((byte) 0, p1, p2, "hardlink@openssh.com");
  }

  private void sendREADLINK(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_READLINK, path);
  }

  private void sendOPENDIR(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_OPENDIR, path);
  }

  private void sendREADDIR(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_READDIR, path);
  }

  private void sendRENAME(byte[] p1, byte[] p2) throws Exception {
    sendPacketPath(SSH_FXP_RENAME, p1, p2,
        extension_posix_rename ? "posix-rename@openssh.com" : null);
  }

  private void sendCLOSE(byte[] path) throws Exception {
    sendPacketPath(SSH_FXP_CLOSE, path);
  }

  private void sendOPENR(byte[] path) throws Exception {
    sendOPEN(path, SSH_FXF_READ);
  }

  private void sendOPENW(byte[] path) throws Exception {
    sendOPEN(path, SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC);
  }

  private void sendOPENA(byte[] path) throws Exception {
    sendOPEN(path, SSH_FXF_WRITE | /* SSH_FXF_APPEND | */ SSH_FXF_CREAT);
  }

  private void sendOPEN(byte[] path, int mode) throws Exception {
    packet.reset();
    putHEAD(SSH_FXP_OPEN, 17 + path.length);
    buf.putInt(seq++);
    buf.putString(path);
    buf.putInt(mode);
    buf.putInt(0); // attrs
    getSession().write(packet, this, 17 + path.length + 4);
  }

  private void sendPacketPath(byte fxp, byte[] path) throws Exception {
    sendPacketPath(fxp, path, (String) null);
  }

  private void sendPacketPath(byte fxp, byte[] path, String extension) throws Exception {
    packet.reset();
    int len = 9 + path.length;
    if (extension == null) {
      putHEAD(fxp, len);
      buf.putInt(seq++);
    } else {
      len += (4 + extension.length());
      putHEAD(SSH_FXP_EXTENDED, len);
      buf.putInt(seq++);
      buf.putString(Util.str2byte(extension));
    }
    buf.putString(path); // path
    getSession().write(packet, this, len + 4);
  }

  private void sendPacketPath(byte fxp, byte[] p1, byte[] p2) throws Exception {
    sendPacketPath(fxp, p1, p2, null);
  }

  private void sendPacketPath(byte fxp, byte[] p1, byte[] p2, String extension) throws Exception {
    packet.reset();
    int len = 13 + p1.length + p2.length;
    if (extension == null) {
      putHEAD(fxp, len);
      buf.putInt(seq++);
    } else {
      len += (4 + extension.length());
      putHEAD(SSH_FXP_EXTENDED, len);
      buf.putInt(seq++);
      buf.putString(Util.str2byte(extension));
    }
    buf.putString(p1);
    buf.putString(p2);
    getSession().write(packet, this, len + 4);
  }

  private int sendWRITE(byte[] handle, long offset, byte[] data, int start, int length)
      throws Exception {
    int _length = length;
    opacket.reset();
    Session _session = getSession();
    int buffer_margin = _session.getBufferMargin();
    if (obuf.buffer.length < obuf.index + 13 + 21 + handle.length + length + buffer_margin) {
      _length = obuf.buffer.length - (obuf.index + 13 + 21 + handle.length + buffer_margin);
      // System.err.println("_length="+_length+" length="+length);
    }

    putHEAD(obuf, SSH_FXP_WRITE, 21 + handle.length + _length); // 14
    obuf.putInt(seq++); // 4
    obuf.putString(handle); // 4+handle.length
    obuf.putLong(offset); // 8
    if (obuf.buffer != data) {
      obuf.putString(data, start, _length); // 4+_length
    } else {
      obuf.putInt(_length);
      obuf.skip(_length);
    }
    _session.write(opacket, this, 21 + handle.length + _length + 4);
    return _length;
  }

  private void sendREAD(byte[] handle, long offset, int length) throws Exception {
    sendREAD(handle, offset, length, null);
  }

  private void sendREAD(byte[] handle, long offset, int length, RequestQueue rrq) throws Exception {
    packet.reset();
    putHEAD(SSH_FXP_READ, 21 + handle.length);
    buf.putInt(seq++);
    buf.putString(handle);
    buf.putLong(offset);
    buf.putInt(length);
    getSession().write(packet, this, 21 + handle.length + 4);
    if (rrq != null) {
      rrq.add(seq - 1, offset, length);
    }
  }

  private void putHEAD(Buffer buf, byte type, int length) throws Exception {
    buf.putByte((byte) Session.SSH_MSG_CHANNEL_DATA);
    buf.putInt(recipient);
    buf.putInt(length + 4);
    buf.putInt(length);
    buf.putByte(type);
  }

  private void putHEAD(byte type, int length) throws Exception {
    putHEAD(buf, type, length);
  }

  private Vector<String> glob_remote(String _path) throws Exception {
    Vector<String> v = new Vector<>();
    int i = 0;

    int foo = _path.lastIndexOf('/');
    if (foo < 0) { // it is not absolute path.
      v.addElement(Util.unquote(_path));
      return v;
    }

    String dir = _path.substring(0, ((foo == 0) ? 1 : foo));
    String _pattern = _path.substring(foo + 1);

    dir = Util.unquote(dir);

    byte[] pattern = null;
    byte[][] _pattern_utf8 = new byte[1][];
    boolean pattern_has_wildcard = isPattern(_pattern, _pattern_utf8);

    if (!pattern_has_wildcard) {
      if (!dir.equals("/"))
        dir += "/";
      v.addElement(dir + Util.unquote(_pattern));
      return v;
    }

    pattern = _pattern_utf8[0];

    sendOPENDIR(Util.str2byte(dir, fEncoding));

    Header header = new Header();
    header = header(buf, header);
    int length = header.length;
    int type = header.type;

    fill(buf, length);

    if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
      throw new SftpException(SSH_FX_FAILURE, "");
    }
    if (type == SSH_FXP_STATUS) {
      i = buf.getInt();
      throwStatusError(buf, i);
    }

    byte[] handle = buf.getString(); // filename
    String pdir = null; // parent directory

    while (true) {
      sendREADDIR(handle);
      header = header(buf, header);
      length = header.length;
      type = header.type;

      if (type != SSH_FXP_STATUS && type != SSH_FXP_NAME) {
        throw new SftpException(SSH_FX_FAILURE, "");
      }
      if (type == SSH_FXP_STATUS) {
        fill(buf, length);
        break;
      }

      buf.rewind();
      fill(buf.buffer, 0, 4);
      length -= 4;
      int count = buf.getInt();

      byte[] str;
      int flags;

      buf.reset();
      while (count > 0) {
        if (length > 0) {
          buf.shift();
          int j =
              (buf.buffer.length > (buf.index + length)) ? length : (buf.buffer.length - buf.index);
          i = io_in.read(buf.buffer, buf.index, j);
          if (i <= 0)
            break;
          buf.index += i;
          length -= i;
        }

        byte[] filename = buf.getString();
        // System.err.println("filename: "+new String(filename));
        if (server_version <= 3) {
          str = buf.getString(); // longname
        }
        SftpATTRS attrs = SftpATTRS.getATTR(buf);

        byte[] _filename = filename;
        String f = null;
        boolean found = false;

        if (!fEncoding_is_utf8) {
          f = Util.byte2str(filename, fEncoding);
          _filename = Util.str2byte(f, StandardCharsets.UTF_8);
        }
        found = Util.glob(pattern, _filename);

        if (found) {
          if (f == null) {
            f = Util.byte2str(filename, fEncoding);
          }
          if (pdir == null) {
            pdir = dir;
            if (!pdir.endsWith("/")) {
              pdir += "/";
            }
          }
          v.addElement(pdir + f);
        }
        count--;
      }
    }
    if (_sendCLOSE(handle, header))
      return v;
    return null;
  }

  private boolean isPattern(byte[] path) {
    int length = path.length;
    int i = 0;
    while (i < length) {
      if (path[i] == '*' || path[i] == '?')
        return true;
      if (path[i] == '\\' && (i + 1) < length)
        i++;
      i++;
    }
    return false;
  }

  private Vector<String> glob_local(String _path) throws Exception {
    // System.err.println("glob_local: "+_path);
    Vector<String> v = new Vector<>();
    byte[] path = Util.str2byte(_path, StandardCharsets.UTF_8);
    int i = path.length - 1;
    while (i >= 0) {
      if (path[i] != '*' && path[i] != '?') {
        i--;
        continue;
      }
      if (!fs_is_bs && i > 0 && path[i - 1] == '\\') {
        i--;
        if (i > 0 && path[i - 1] == '\\') {
          i--;
          i--;
          continue;
        }
      }
      break;
    }

    if (i < 0) {
      v.addElement(fs_is_bs ? _path : Util.unquote(_path));
      return v;
    }

    while (i >= 0) {
      if (path[i] == file_separatorc || (fs_is_bs && path[i] == '/')) { // On Windows, '/' is also
                                                                        // the separator.
        break;
      }
      i--;
    }

    if (i < 0) {
      v.addElement(fs_is_bs ? _path : Util.unquote(_path));
      return v;
    }

    byte[] dir;
    if (i == 0) {
      dir = new byte[] {(byte) file_separatorc};
    } else {
      dir = new byte[i];
      System.arraycopy(path, 0, dir, 0, i);
    }

    byte[] pattern = new byte[path.length - i - 1];
    System.arraycopy(path, i + 1, pattern, 0, pattern.length);

    // System.err.println("dir: "+new String(dir)+" pattern: "+new String(pattern));
    try {
      String[] children = (new File(Util.byte2str(dir, StandardCharsets.UTF_8))).list();
      String pdir = Util.byte2str(dir) + file_separator;
      for (int j = 0; j < children.length; j++) {
        // System.err.println("children: "+children[j]);
        if (Util.glob(pattern, Util.str2byte(children[j], StandardCharsets.UTF_8))) {
          v.addElement(pdir + children[j]);
        }
      }
    } catch (Exception e) {
    }
    return v;
  }

  private void throwStatusError(Buffer buf, int i) throws SftpException {
    if (server_version >= 3 && // WindRiver's sftp will send invalid
        buf.getLength() >= 4) { // SSH_FXP_STATUS packet.
      byte[] str = buf.getString();
      // byte[] tag=buf.getString();
      throw new SftpException(i, Util.byte2str(str, StandardCharsets.UTF_8));
    } else {
      throw new SftpException(i, "Failure");
    }
  }

  private static boolean isLocalAbsolutePath(String path) {
    return (new File(path)).isAbsolute();
  }

  @Override
  public void disconnect() {
    super.disconnect();
  }

  private boolean isPattern(String path, byte[][] utf8) {
    byte[] _path = Util.str2byte(path, StandardCharsets.UTF_8);
    if (utf8 != null)
      utf8[0] = _path;
    return isPattern(_path);
  }

  private boolean isPattern(String path) {
    return isPattern(path, null);
  }

  private void fill(Buffer buf, int len) throws IOException {
    buf.reset();
    fill(buf.buffer, 0, len);
    buf.skip(len);
  }

  private int fill(byte[] buf, int s, int len) throws IOException {
    int i = 0;
    int foo = s;
    while (len > 0) {
      i = io_in.read(buf, s, len);
      if (i <= 0) {
        throw new IOException("inputstream is closed");
        // return (s-foo)==0 ? i : s-foo;
      }
      s += i;
      len -= i;
    }
    return s - foo;
  }

  private void skip(long foo) throws IOException {
    while (foo > 0) {
      long bar = io_in.skip(foo);
      if (bar <= 0)
        break;
      foo -= bar;
    }
  }

  static class Header {
    int length;
    int type;
    int rid;
  }

  private Header header(Buffer buf, Header header) throws IOException {
    buf.rewind();
    int i = fill(buf.buffer, 0, 9);
    header.length = buf.getInt() - 5;
    header.type = buf.getByte() & 0xff;
    header.rid = buf.getInt();
    return header;
  }

  private String remoteAbsolutePath(String path) throws SftpException {
    if (path.charAt(0) == '/')
      return path;
    String cwd = getCwd();
    // if(cwd.equals(getHome())) return path;
    if (cwd.endsWith("/"))
      return cwd + path;
    return cwd + "/" + path;
  }

  private String localAbsolutePath(String path) {
    if (isLocalAbsolutePath(path))
      return path;
    if (lcwd.endsWith(file_separator))
      return lcwd + path;
    return lcwd + file_separator + path;
  }

  /**
   * This method will check if the given string can be expanded to the unique string. If it can be
   * expanded to mutiple files, SftpException will be thrown.
   *
   * @return the returned string is unquoted.
   */
  private String isUnique(String path) throws SftpException, Exception {
    Vector<String> v = glob_remote(path);
    if (v.size() != 1) {
      throw new SftpException(SSH_FX_FAILURE, path + " is not unique: " + v.toString());
    }
    return v.elementAt(0);
  }

  public int getServerVersion() throws SftpException {
    if (!isConnected()) {
      throw new SftpException(SSH_FX_FAILURE, "The channel is not connected.");
    }
    return server_version;
  }

  @Deprecated
  public void setFilenameEncoding(String encoding) throws SftpException {
    try {
      setFilenameEncoding(Charset.forName(encoding));
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public void setFilenameEncoding(Charset encoding) {
    fEncoding = encoding;
    fEncoding_is_utf8 = fEncoding.equals(StandardCharsets.UTF_8);
  }

  public String getExtension(String key) {
    if (extensions == null)
      return null;
    return extensions.get(key);
  }

  public String realpath(String path) throws SftpException {
    try {
      byte[] _path = _realpath(remoteAbsolutePath(path));
      return Util.byte2str(_path, fEncoding);
    } catch (Exception e) {
      if (e instanceof SftpException)
        throw (SftpException) e;
      throw new SftpException(SSH_FX_FAILURE, e.toString(), e);
    }
  }

  public static class LsEntry implements Comparable<LsEntry> {
    private String filename;
    private String longname;
    private SftpATTRS attrs;

    LsEntry(String filename, String longname, SftpATTRS attrs) {
      setFilename(filename);
      setLongname(longname);
      setAttrs(attrs);
    }

    public String getFilename() {
      return filename;
    };

    void setFilename(String filename) {
      this.filename = filename;
    };

    public String getLongname() {
      return longname;
    };

    void setLongname(String longname) {
      this.longname = longname;
    };

    public SftpATTRS getAttrs() {
      return attrs;
    };

    void setAttrs(SftpATTRS attrs) {
      this.attrs = attrs;
    };

    @Override
    public String toString() {
      return longname;
    }

    @Override
    public int compareTo(LsEntry o) {
      return filename.compareTo(o.getFilename());
    }
  }

  /**
   * This interface will be passed as an argument for <code>ls</code> method.
   *
   * @see ChannelSftp.LsEntry
   * @see #ls(String, ChannelSftp.LsEntrySelector)
   * @since 0.1.47
   */
  public interface LsEntrySelector {
    public final int CONTINUE = 0;
    public final int BREAK = 1;

    /**
     * The <code>select</code> method will be invoked in <code>ls</code> method for each file entry.
     * If this method returns BREAK, <code>ls</code> will be canceled.
     *
     * @param entry one of entry from ls
     * @return if BREAK is returned, the 'ls' operation will be canceled.
     */
    public int select(LsEntry entry);
  }
}
