Newer
Older
* psftp.c: (platform-independent) front end for PSFTP.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <assert.h>
#include <limits.h>
#include "psftp.h"
#include "storage.h"
#include "ssh.h"
#include "sftp.h"
const char *const appname = "PSFTP";
/*
* Since SFTP is a request-response oriented protocol, it requires
* no buffer management: when we send data, we stop and wait for an
* acknowledgement _anyway_, and so we can't possibly overfill our
* send buffer.
*/
static int psftp_connect(char *userhost, char *user, int portnumber);
static int do_sftp_init(void);
static void do_sftp_cleanup(void);
/* ----------------------------------------------------------------------
* sftp client state.
*/
/* ------------------------------------------------------------
* Seat vtable.
*/
static size_t psftp_output(Seat *, bool is_stderr, const void *, size_t);
static bool psftp_eof(Seat *);
static const SeatVtable psftp_seat_vt = {
.output = psftp_output,
.eof = psftp_eof,
.get_userpass_input = filexfer_get_userpass_input,
.notify_remote_exit = nullseat_notify_remote_exit,
.connection_fatal = console_connection_fatal,
.update_specials_menu = nullseat_update_specials_menu,
.get_ttymode = nullseat_get_ttymode,
.set_busy_status = nullseat_set_busy_status,
.verify_ssh_host_key = console_verify_ssh_host_key,
.confirm_weak_crypto_primitive = console_confirm_weak_crypto_primitive,
.confirm_weak_cached_hostkey = console_confirm_weak_cached_hostkey,
.is_utf8 = nullseat_is_never_utf8,
.echoedit_update = nullseat_echoedit_update,
.get_x_display = nullseat_get_x_display,
.get_windowid = nullseat_get_windowid,
.get_window_pixel_size = nullseat_get_window_pixel_size,
.stripctrl_new = console_stripctrl_new,
.set_trust_status = nullseat_set_trust_status_vacuously,
.verbose = cmdline_seat_verbose,
.interactive = nullseat_interactive_yes,
.get_cursor_position = nullseat_get_cursor_position,
};
static Seat psftp_seat[1] = {{ &psftp_seat_vt }};
/* ----------------------------------------------------------------------
* A nasty loop macro that lets me get an escape-sequence sanitised
* version of a string for display, and free it automatically
* afterwards.
*/
static StripCtrlChars *string_scc;
#define with_stripctrl(varname, input) \
for (char *varname = stripctrl_string(string_scc, input); varname; \
sfree(varname), varname = NULL)
/* ----------------------------------------------------------------------
* Manage sending requests and waiting for replies.
*/
struct sftp_packet *sftp_wait_for_reply(struct sftp_request *req)
{
struct sftp_packet *pktin;
struct sftp_request *rreq;
sftp_register(req);
pktin = sftp_recv();
if (pktin == NULL) {
seat_connection_fatal(
psftp_seat, "did not receive SFTP response packet from server");
}
rreq = sftp_find_request(pktin);
if (rreq != req) {
seat_connection_fatal(
psftp_seat,
"unable to understand SFTP response packet from server: %s",
fxp_error());
}
return pktin;
}
/* ----------------------------------------------------------------------
* Higher-level helper functions used in commands.
*/
/*
* Attempt to canonify a pathname starting from the pwd. If
* canonification fails, at least fall back to returning a _valid_
* pathname (though it may be ugly, eg /home/simon/../foobar).
char *fullname, *canonname;
struct sftp_packet *pktin;
struct sftp_request *req;
if (name[0] == '/') {
const char *slash;
if (pwd[strlen(pwd) - 1] == '/')
slash = "";
else
slash = "/";
fullname = dupcat(pwd, slash, name);
req = fxp_realpath_send(fullname);
pktin = sftp_wait_for_reply(req);
canonname = fxp_realpath_recv(pktin, req);
if (canonname) {
sfree(fullname);
return canonname;
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/*
* Attempt number 2. Some FXP_REALPATH implementations
* (glibc-based ones, in particular) require the _whole_
* path to point to something that exists, whereas others
* (BSD-based) only require all but the last component to
* exist. So if the first call failed, we should strip off
* everything from the last slash onwards and try again,
* then put the final component back on.
*
* Special cases:
*
* - if the last component is "/." or "/..", then we don't
* bother trying this because there's no way it can work.
*
* - if the thing actually ends with a "/", we remove it
* before we start. Except if the string is "/" itself
* (although I can't see why we'd have got here if so,
* because surely "/" would have worked the first
* time?), in which case we don't bother.
*
* - if there's no slash in the string at all, give up in
* confusion (we expect at least one because of the way
* we constructed the string).
*/
int i;
char *returnname;
i = strlen(fullname);
if (i > 2 && fullname[i - 1] == '/')
fullname[--i] = '\0'; /* strip trailing / unless at pos 0 */
while (i > 0 && fullname[--i] != '/');
/*
* Give up on special cases.
*/
if (fullname[i] != '/' || /* no slash at all */
!strcmp(fullname + i, "/.") || /* ends in /. */
!strcmp(fullname + i, "/..") || /* ends in /.. */
!strcmp(fullname, "/")) {
return fullname;
}
/*
* Now i points at the slash. Deal with the final special
* case i==0 (ie the whole path was "/nonexistentfile").
*/
fullname[i] = '\0'; /* separate the string */
if (i == 0) {
req = fxp_realpath_send("/");
} else {
req = fxp_realpath_send(fullname);
}
pktin = sftp_wait_for_reply(req);
canonname = fxp_realpath_recv(pktin, req);
if (!canonname) {
/* Even that failed. Restore our best guess at the
* constructed filename and give up */
fullname[i] = '/'; /* restore slash and last component */
return fullname;
}
/*
* We have a canonical name for all but the last path
* component. Concatenate the last component and return.
*/
returnname = dupcat(canonname,
(strendswith(canonname, "/") ? "" : "/"),
fullname + i + 1);
sfree(fullname);
sfree(canonname);
return returnname;
}
static int bare_name_compare(const void *av, const void *bv)
const char **a = (const char **) av;
const char **b = (const char **) bv;
return strcmp(*a, *b);
static void not_connected(void)
{
printf("psftp: not connected to a host; use \"open host.name\"\n");
}
/* ----------------------------------------------------------------------
* The meat of the `get' and `put' commands.
bool sftp_get_file(char *fname, char *outfname, bool recurse, bool restart)
struct fxp_handle *fh;
struct sftp_packet *pktin;
struct sftp_request *req;
struct fxp_xfer *xfer;
WFile *file;
bool toret, shown_err = false;
struct fxp_attrs attrs;
/*
* In recursive mode, see if we're dealing with a directory.
* (If we're not in recursive mode, we need not even check: the
* subsequent FXP_OPEN will return a usable error message.)
*/
if (recurse) {
req = fxp_stat_send(fname);
pktin = sftp_wait_for_reply(req);
result = fxp_stat_recv(pktin, req, &attrs);
if (result &&
(attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS) &&
(attrs.permissions & 0040000)) {
struct fxp_handle *dirhandle;
size_t nnames, namesize;
struct fxp_name **ournames;
struct fxp_names *names;
int i;
/*
* First, attempt to create the destination directory,
* unless it already exists.
*/
if (file_type(outfname) != FILE_TYPE_DIRECTORY &&
!create_directory(outfname)) {
with_stripctrl(san, outfname)
printf("%s: Cannot create directory\n", san);
/*
* Now get the list of filenames in the remote
* directory.
*/
req = fxp_opendir_send(fname);
pktin = sftp_wait_for_reply(req);
dirhandle = fxp_opendir_recv(pktin, req);
with_stripctrl(san, fname)
printf("%s: unable to open directory: %s\n",
san, fxp_error());
return false;
}
nnames = namesize = 0;
ournames = NULL;
while (1) {
int i;
req = fxp_readdir_send(dirhandle);
pktin = sftp_wait_for_reply(req);
names = fxp_readdir_recv(pktin, req);
if (names == NULL) {
if (fxp_error_type() == SSH_FX_EOF)
break;
with_stripctrl(san, fname)
printf("%s: reading directory: %s\n",
san, fxp_error());
req = fxp_close_send(dirhandle);
pktin = sftp_wait_for_reply(req);
fxp_close_recv(pktin, req);
sfree(ournames);
return false;
}
if (names->nnames == 0) {
fxp_free_names(names);
break;
}
sgrowarrayn(ournames, namesize, nnames, names->nnames);
for (i = 0; i < names->nnames; i++)
if (strcmp(names->names[i].filename, ".") &&
strcmp(names->names[i].filename, "..")) {
if (!vet_filename(names->names[i].filename)) {
with_stripctrl(san, names->names[i].filename)
printf("ignoring potentially dangerous server-"
"supplied filename '%s'\n", san);
} else {
ournames[nnames++] =
fxp_dup_name(&names->names[i]);
}
}
fxp_free_names(names);
}
req = fxp_close_send(dirhandle);
pktin = sftp_wait_for_reply(req);
fxp_close_recv(pktin, req);
/*
* Sort the names into a clear order. This ought to
* make things more predictable when we're doing a
* reget of the same directory, just in case two
* readdirs on the same remote directory return a
* different order.
*/
if (nnames > 0)
qsort(ournames, nnames, sizeof(*ournames), sftp_name_compare);
/*
* If we're in restart mode, find the last filename on
* this list that already exists. We may have to do a
* reget on _that_ file, but shouldn't have to do
* anything on the previous files.
*
* If none of them exists, of course, we start at 0.
*/
i = 0;
if (restart) {
while (i < nnames) {
char *nextoutfname;
nextoutfname = dir_file_cat(outfname,
ournames[i]->filename);
nonexistent = (file_type(nextoutfname) ==
FILE_TYPE_NONEXISTENT);
sfree(nextoutfname);
break;
i++;
}
if (i > 0)
i--;
}
/*
* Now we're ready to recurse. Starting at ournames[i]
* and continuing on to the end of the list, we
* construct a new source and target file name, and
* call sftp_get_file again.
*/
for (; i < nnames; i++) {
char *nextfname, *nextoutfname;
bool retd;
nextfname = dupcat(fname, "/", ournames[i]->filename);
nextoutfname = dir_file_cat(outfname, ournames[i]->filename);
nextfname, nextoutfname, recurse, restart);
restart = false; /* after first partial file, do full */
sfree(nextoutfname);
sfree(nextfname);
if (!retd) {
for (i = 0; i < nnames; i++) {
fxp_free_name(ournames[i]);
}
sfree(ournames);
return false;
}
}
/*
* Done this recursion level. Free everything.
*/
for (i = 0; i < nnames; i++) {
fxp_free_name(ournames[i]);
}
sfree(ournames);
return true;
}
}
req = fxp_stat_send(fname);
pktin = sftp_wait_for_reply(req);
if (!fxp_stat_recv(pktin, req, &attrs))
attrs.flags = 0;
req = fxp_open_send(fname, SSH_FXF_READ, NULL);
pktin = sftp_wait_for_reply(req);
fh = fxp_open_recv(pktin, req);
if (!fh) {
with_stripctrl(san, fname)
printf("%s: open for read: %s\n", san, fxp_error());
if (restart) {
file = open_existing_wfile(outfname, NULL);
} else {
file = open_new_file(outfname, GET_PERMISSIONS(attrs, -1));
}
if (!file) {
with_stripctrl(san, outfname)
printf("local: unable to open %s\n", san);
req = fxp_close_send(fh);
pktin = sftp_wait_for_reply(req);
fxp_close_recv(pktin, req);
}
if (restart) {
if (seek_file(file, 0, FROM_END) == -1) {
close_wfile(file);
with_stripctrl(san, outfname)
printf("reget: cannot restart %s - file too large\n", san);
req = fxp_close_send(fh);
pktin = sftp_wait_for_reply(req);
fxp_close_recv(pktin, req);
return false;
}
offset = get_file_posn(file);
printf("reget: restarting at file position %"PRIu64"\n", offset);
} else {
with_stripctrl(san, fname) {
with_stripctrl(sano, outfname)
printf("remote:%s => local:%s\n", san, sano);
}
/*
* FIXME: we can use FXP_FSTAT here to get the file size, and
* thus put up a progress bar.
*/
xfer = xfer_download_init(fh, offset);
while (!xfer_done(xfer)) {
void *vbuf;
int retd, len;
int wpos, wlen;
xfer_download_queue(xfer);
pktin = sftp_recv();
retd = xfer_download_gotpkt(xfer, pktin);
if (retd <= 0) {
if (!shown_err) {
printf("error while reading: %s\n", fxp_error());
shown_err = true;
}
if (retd == INT_MIN) /* pktin not even freed */
sfree(pktin);
}
while (xfer_download_data(xfer, &vbuf, &len)) {
unsigned char *buf = (unsigned char *)vbuf;
wpos = 0;
while (wpos < len) {
wlen = write_to_file(file, buf + wpos, len - wpos);
if (wlen <= 0) {
printf("error while writing local file\n");
toret = false;
xfer_set_error(xfer);
break;
}
wpos += wlen;
}
if (wpos < len) { /* we had an error */
toret = false;
xfer_set_error(xfer);
}
sfree(vbuf);
}
}
xfer_cleanup(xfer);
close_wfile(file);
req = fxp_close_send(fh);
pktin = sftp_wait_for_reply(req);
fxp_close_recv(pktin, req);
}
bool sftp_put_file(char *fname, char *outfname, bool recurse, bool restart)
struct fxp_handle *fh;
struct fxp_xfer *xfer;
struct sftp_packet *pktin;
struct sftp_request *req;
RFile *file;
struct fxp_attrs attrs;
long permissions;
/*
* In recursive mode, see if we're dealing with a directory.
* (If we're not in recursive mode, we need not even check: the
* subsequent fopen will return an error message.)
*/
if (recurse && file_type(fname) == FILE_TYPE_DIRECTORY) {
bool result;
size_t nnames, namesize;
char *name, **ournames;
const char *opendir_err;
/*
* First, attempt to create the destination directory,
* unless it already exists.
*/
req = fxp_stat_send(outfname);
pktin = sftp_wait_for_reply(req);
result = fxp_stat_recv(pktin, req, &attrs);
if (!result ||
!(attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS) ||
!(attrs.permissions & 0040000)) {
req = fxp_mkdir_send(outfname, NULL);
pktin = sftp_wait_for_reply(req);
result = fxp_mkdir_recv(pktin, req);
if (!result) {
printf("%s: create directory: %s\n",
outfname, fxp_error());
return false;
}
}
/*
* Now get the list of filenames in the local directory.
*/
nnames = namesize = 0;
ournames = NULL;
dh = open_directory(fname, &opendir_err);
if (!dh) {
printf("%s: unable to open directory: %s\n", fname, opendir_err);
return false;
}
while ((name = read_filename(dh)) != NULL) {
sgrowarray(ournames, namesize, nnames);
ournames[nnames++] = name;
}
close_directory(dh);
/*
* Sort the names into a clear order. This ought to make
* things more predictable when we're doing a reput of the
* same directory, just in case two readdirs on the same
* local directory return a different order.
*/
if (nnames > 0)
qsort(ournames, nnames, sizeof(*ournames), bare_name_compare);
/*
* If we're in restart mode, find the last filename on this
* list that already exists. We may have to do a reput on
* _that_ file, but shouldn't have to do anything on the
* previous files.
*
* If none of them exists, of course, we start at 0.
*/
i = 0;
if (restart) {
while (i < nnames) {
char *nextoutfname;
nextoutfname = dupcat(outfname, "/", ournames[i]);
req = fxp_stat_send(nextoutfname);
pktin = sftp_wait_for_reply(req);
result = fxp_stat_recv(pktin, req, &attrs);
sfree(nextoutfname);
if (!result)
break;
i++;
}
if (i > 0)
i--;
}
/*
* Now we're ready to recurse. Starting at ournames[i]
* and continuing on to the end of the list, we
* construct a new source and target file name, and
* call sftp_put_file again.
*/
for (; i < nnames; i++) {
char *nextfname, *nextoutfname;
nextfname = dir_file_cat(fname, ournames[i]);
nextoutfname = dupcat(outfname, "/", ournames[i]);
retd = sftp_put_file(nextfname, nextoutfname, recurse, restart);
restart = false; /* after first partial file, do full */
sfree(nextoutfname);
sfree(nextfname);
if (!retd) {
for (size_t i = 0; i < nnames; i++) {
sfree(ournames[i]);
}
sfree(ournames);
return false;
}
}
/*
* Done this recursion level. Free everything.
*/
for (size_t i = 0; i < nnames; i++) {
sfree(ournames[i]);
}
sfree(ournames);
return true;
}
file = open_existing_file(fname, NULL, NULL, NULL, &permissions);
if (!file) {
printf("local: unable to open %s\n", fname);
return false;
attrs.flags = 0;
PUT_PERMISSIONS(attrs, permissions);
if (restart) {
req = fxp_open_send(outfname, SSH_FXF_WRITE, &attrs);
} else {
req = fxp_open_send(outfname,
SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC,
&attrs);
pktin = sftp_wait_for_reply(req);
fh = fxp_open_recv(pktin, req);
if (!fh) {
close_rfile(file);
printf("%s: open for write: %s\n", outfname, fxp_error());
return false;
}
if (restart) {
req = fxp_fstat_send(fh);
pktin = sftp_wait_for_reply(req);
retd = fxp_fstat_recv(pktin, req, &attrs);
if (!retd) {
printf("read size of %s: %s\n", outfname, fxp_error());
err = true;
goto cleanup;
}
if (!(attrs.flags & SSH_FILEXFER_ATTR_SIZE)) {
printf("read size of %s: size was not given\n", outfname);
err = true;
goto cleanup;
}
offset = attrs.size;
printf("reput: restarting at file position %"PRIu64"\n", offset);
if (seek_file((WFile *)file, offset, FROM_START) != 0)
seek_file((WFile *)file, 0, FROM_END); /* *shrug* */
} else {
printf("local:%s => remote:%s\n", fname, outfname);
/*
* FIXME: we can use FXP_FSTAT here to get the file size, and
* thus put up a progress bar.
*/
xfer = xfer_upload_init(fh, offset);
while ((!err && !eof) || !xfer_done(xfer)) {
char buffer[4096];
int len, ret;
while (xfer_upload_ready(xfer) && !err && !eof) {
len = read_from_file(file, buffer, sizeof(buffer));
if (len == -1) {
printf("error while reading local file\n");
err = true;
} else if (len == 0) {
eof = true;
} else {
xfer_upload_data(xfer, buffer, len);
}
}
if (toplevel_callback_pending() && !err && !eof) {
/* If we have pending callbacks, they might make
* xfer_upload_ready start to return true. So we should
* run them and then re-check xfer_upload_ready, before
* we go as far as waiting for an entire packet to
* arrive. */
run_toplevel_callbacks();
continue;
}
if (!xfer_done(xfer)) {
pktin = sftp_recv();
ret = xfer_upload_gotpkt(xfer, pktin);
if (ret <= 0) {
if (ret == INT_MIN) /* pktin not even freed */
sfree(pktin);
if (!err) {
printf("error while writing: %s\n", fxp_error());
}
xfer_cleanup(xfer);
req = fxp_close_send(fh);
pktin = sftp_wait_for_reply(req);
if (!fxp_close_recv(pktin, req)) {
if (!err) {
printf("error while closing: %s", fxp_error());
err = true;
}
close_rfile(file);
/* ----------------------------------------------------------------------
* A remote wildcard matcher, providing a similar interface to the
* local one in psftp.h.
*/
typedef struct SftpWildcardMatcher {
struct fxp_handle *dirh;
struct fxp_names *names;
int namepos;
char *wildcard, *prefix;
} SftpWildcardMatcher;
SftpWildcardMatcher *sftp_begin_wildcard_matching(char *name)
{
struct sftp_packet *pktin;
struct sftp_request *req;
char *wildcard;
char *unwcdir, *tmpdir, *cdir;
SftpWildcardMatcher *swcm;
struct fxp_handle *dirh;
/*
* We don't handle multi-level wildcards; so we expect to find
* a fully specified directory part, followed by a wildcard
* after that.
*/
wildcard = stripslashes(name, false);
unwcdir = dupstr(name);
len = wildcard - name;
unwcdir[len] = '\0';
if (len > 0 && unwcdir[len-1] == '/')
tmpdir = snewn(1 + len, char);
check = wc_unescape(tmpdir, unwcdir);
sfree(tmpdir);
if (!check) {
printf("Multiple-level wildcards are not supported\n");
sfree(unwcdir);
return NULL;
}
cdir = canonify(unwcdir);
req = fxp_opendir_send(cdir);
pktin = sftp_wait_for_reply(req);
dirh = fxp_opendir_recv(pktin, req);
if (dirh) {
swcm = snew(SftpWildcardMatcher);
swcm->dirh = dirh;
swcm->names = NULL;
swcm->wildcard = dupstr(wildcard);
swcm->prefix = unwcdir;
} else {
printf("Unable to open %s: %s\n", cdir, fxp_error());
swcm = NULL;
sfree(unwcdir);
}
sfree(cdir);
return swcm;
}
char *sftp_wildcard_get_filename(SftpWildcardMatcher *swcm)
{
struct fxp_name *name;
struct sftp_packet *pktin;
struct sftp_request *req;
while (1) {
if (swcm->names && swcm->namepos >= swcm->names->nnames) {
fxp_free_names(swcm->names);
swcm->names = NULL;
}
if (!swcm->names) {
req = fxp_readdir_send(swcm->dirh);
pktin = sftp_wait_for_reply(req);
swcm->names = fxp_readdir_recv(pktin, req);
if (!swcm->names) {
if (fxp_error_type() != SSH_FX_EOF) {
with_stripctrl(san, swcm->prefix)
printf("%s: reading directory: %s\n",
san, fxp_error());
}
return NULL;
} else if (swcm->names->nnames == 0) {
/*
* Another failure mode which we treat as EOF is if
* the server reports success from FXP_READDIR but
* returns no actual names. This is unusual, since
* from most servers you'd expect at least "." and
* "..", but there's nothing forbidding a server from
* omitting those if it wants to.
*/
return NULL;
}
assert(swcm->names && swcm->namepos < swcm->names->nnames);
name = &swcm->names->names[swcm->namepos++];
if (!strcmp(name->filename, ".") || !strcmp(name->filename, ".."))
continue; /* expected bad filenames */
if (!vet_filename(name->filename)) {
with_stripctrl(san, name->filename)
printf("ignoring potentially dangerous server-"
"supplied filename '%s'\n", san);
continue; /* unexpected bad filename */
}
if (!wc_match(swcm->wildcard, name->filename))
continue; /* doesn't match the wildcard */
/*
* We have a working filename. Return it.
*/
return dupprintf("%s%s%s", swcm->prefix,
(!swcm->prefix[0] ||
swcm->prefix[strlen(swcm->prefix)-1]=='/' ?
"" : "/"),
name->filename);
}
}
void sftp_finish_wildcard_matching(SftpWildcardMatcher *swcm)
{
struct sftp_packet *pktin;
struct sftp_request *req;
req = fxp_close_send(swcm->dirh);
pktin = sftp_wait_for_reply(req);
fxp_close_recv(pktin, req);
if (swcm->names)
fxp_free_names(swcm->names);
sfree(swcm->prefix);
sfree(swcm->wildcard);
sfree(swcm);
}
/*
* General function to match a potential wildcard in a filename
* argument and iterate over every matching file. Used in several
* PSFTP commands (rmdir, rm, chmod, mv).
*/
bool wildcard_iterate(char *filename, bool (*func)(void *, char *), void *ctx)
{
char *unwcfname, *newname, *cname;
unwcfname = snewn(strlen(filename)+1, char);
is_wc = !wc_unescape(unwcfname, filename);
if (is_wc) {
SftpWildcardMatcher *swcm = sftp_begin_wildcard_matching(filename);
bool matched = false;
sfree(unwcfname);
if (!swcm)
return false;
while ( (newname = sftp_wildcard_get_filename(swcm)) != NULL ) {
cname = canonify(newname);
sfree(newname);
if (!func(ctx, cname))
toret = false;
if (!matched) {
/* Politely warn the user that nothing matched. */
printf("%s: nothing matched\n", filename);
}
sftp_finish_wildcard_matching(swcm);
} else {
cname = canonify(unwcfname);
toret = func(ctx, cname);
sfree(cname);
sfree(unwcfname);
}
}
/*
* Handy helper function.
*/
{
char *unwcfname = snewn(strlen(name)+1, char);
bool is_wc = !wc_unescape(unwcfname, name);
sfree(unwcfname);
return is_wc;
}
/* ----------------------------------------------------------------------
* Actual sftp commands.
*/
struct sftp_command {
char **words;
size_t nwords, wordssize;
int (*obey) (struct sftp_command *); /* returns <0 to quit */
};
int sftp_cmd_null(struct sftp_command *cmd)
{
}
int sftp_cmd_unknown(struct sftp_command *cmd)
{
printf("psftp: unknown command \"%s\"\n", cmd->words[0]);