mc/vfs/fish.c

915 lines
23 KiB
C

/* Virtual File System: FISH implementation for transfering files over
shell connections.
Copyright (C) 1998 The Free Software Foundation
Written by: 1998 Pavel Machek
Derived from ftpfs.c.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
/*
* Read README.fish for protocol specification.
*
* Syntax of path is: /#sh:user@host[:Cr]/path
* where C means you want compressed connection,
* and r means you want to use rsh
*/
#include <config.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <stdarg.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include <ctype.h> /* For isdigit */
#ifdef SCO_FLAVOR
# include <sys/timeb.h> /* alex: for struct timeb definition */
#endif /* SCO_FLAVOR */
#include <time.h>
#include <sys/types.h>
#if defined(HAVE_UNISTD_H)
#include <unistd.h>
#endif
#if HAVE_SYS_SELECT_H
# include <sys/select.h>
#endif
#include "../src/fs.h"
#include "../src/mad.h"
#include "../src/setup.h"
#include "../src/tty.h" /* enable/disable interrupt key */
#include "../src/main.h"
#ifndef SCO_FLAVOR
# include <sys/time.h> /* alex: this redefines struct timeval */
#endif /* SCO_FLAVOR */
#include <sys/param.h>
#include "../src/mem.h"
#define WANT_PARSE_LS_LGA
#include "vfs.h"
#include "tcputil.h"
#include "../src/util.h"
#include "../src/dialog.h"
#include "container.h"
#include "fish.h"
#ifndef MAXHOSTNAMELEN
# define MAXHOSTNAMELEN 64
#endif
/*
* Reply codes.
*/
#define PRELIM 1 /* positive preliminary */
#define COMPLETE 2 /* positive completion */
#define CONTINUE 3 /* positive intermediate */
#define TRANSIENT 4 /* transient negative completion */
#define ERROR 5 /* permanent negative completion */
#define UPLOAD_ZERO_LENGTH_FILE
static int my_errno;
static int code;
/* Where we store the transactions */
static FILE *logfile = NULL;
/* If true, the directory cache is forced to reload */
static int force_expiration = 0;
static struct linklist *connections_list;
/* command wait_flag: */
#define NONE 0x00
#define WAIT_REPLY 0x01
#define WANT_STRING 0x02
static char reply_str [80];
static char *fish_get_current_directory(struct connection *bucket);
static void free_bucket (void *data);
static void connection_destructor(void *data);
static void flush_all_directory(struct connection *bucket);
static int get_line (int sock, char *buf, int buf_len, char term);
static char *get_path (struct connection **bucket, char *path);
static char *my_get_host_and_username (char *path, char **host, char **user, int *flags, char **pass)
{
return get_host_and_username (path, host, user, flags, 0, 0, pass);
}
static int decode_reply (char *s, int was_garbage)
{
if (!sscanf(s, "%d", &code)) {
code = 500;
return 5;
}
if (code<100) return was_garbage ? ERROR : (!code ? COMPLETE : PRELIM);
return code / 100;
}
/* Returns a reply code, check /usr/include/arpa/ftp.h for possible values */
static int get_reply (int sock, char *string_buf, int string_len)
{
char answer[1024];
int was_garbage = 0;
for (;;) {
if (!get_line(sock, answer, sizeof(answer), '\n')) {
if (string_buf)
*string_buf = 0;
code = 421;
return 4;
}
if (strncmp(answer, "### ", 4)) {
was_garbage = 1;
if (string_buf) {
strncpy(string_buf, answer, string_len - 1);
*(string_buf + string_len - 1) = 0;
}
} else return decode_reply(answer+4, was_garbage);
}
}
int got_sigpipe = 0;
static int command (struct connection *bucket, int wait_reply,
char *fmt, ...)
{
va_list ap;
char buf[2048]; /* FIXME: buffer exceed ?? */
int n, status;
va_start (ap, fmt);
vsprintf (buf, fmt, ap);
va_end (ap);
n = strlen(buf);
buf[n] = 0;
if (logfile){
fwrite (buf, strlen (buf), 1, logfile);
fflush (logfile);
}
enable_interrupt_key();
status = write(qsockw(bucket), buf, strlen(buf));
if (status < 0){
code = 421;
if (errno == EPIPE){
got_sigpipe = 1;
}
disable_interrupt_key();
return TRANSIENT;
}
disable_interrupt_key();
if (wait_reply)
return get_reply (qsockr(bucket), (wait_reply & WANT_STRING) ? reply_str : NULL, sizeof (reply_str)-1);
return COMPLETE;
}
static void
connection_close (void *data)
{
struct connection *bucket = data;
if ((qsockw (bucket) != -1) || (qsockr (bucket) != -1)){
print_vfs_message ("fish: Disconnecting from %s", qhost(bucket));
command(bucket, NONE, "logout\n");
close(qsockw(bucket));
close(qsockr(bucket));
}
}
static void
pipeopen(struct connection *bucket, char *path, char *argv[])
{
int fileset1[2], fileset2[2];
FILE *retval;
int res;
if (pipe(fileset1)<0) vfs_die("Could not pipe(): %m.");
if (pipe(fileset2)<0) vfs_die("Could not pipe(): %m.");
if (res = fork()) {
if (res<0) vfs_die("Could not fork(): %m.");
/* We are the parent */
close(fileset1[0]);
qsockw(bucket) = fileset1[1];
close(fileset2[1]);
qsockr(bucket) = fileset2[0];
if (!retval) vfs_die( "Could not fdopen(): %m." );
} else {
close(0);
dup(fileset1[0]);
close(fileset1[0]); close(fileset1[1]);
close(1); close(2);
dup(fileset2[1]);
dup(fileset2[1]);
close(fileset2[0]); close(fileset2[1]);
execvp(path, argv);
vfs_die("Exec failed.");
}
}
static struct connection *
open_command_connection (char *host, char *user, int flags, char *netrcpass)
{
struct connection *bucket;
char *argv[100];
int i = 0;
bucket = xmalloc(sizeof(struct connection),
"struct connection");
if (bucket == NULL) ERRNOR (ENOMEM, NULL);
qhost(bucket) = strdup (host);
quser(bucket) = strdup (user);
qcdir(bucket) = NULL;
qflags(bucket) = flags;
qlock(bucket) = 0;
qhome(bucket) = NULL;
qupdir(bucket)= 0;
qdcache(bucket)=0;
bucket->__inode_counter = 0;
bucket->lock = 0;
bucket->password = 0;
my_errno = ENOMEM;
if ((qdcache(bucket) = linklist_init()) == NULL)
goto error;
#define XSH (flags == FISH_FLAG_RSH ? "rsh" : "ssh")
argv[i++] = XSH;
argv[i++] = "-l";
argv[i++] = user;
argv[i++] = host;
if (flags == FISH_FLAG_COMPRESSED)
argv[i++] = "-C";
argv[i++] = "echo FISH:; /bin/sh";
argv[i++] = NULL;
pipeopen(bucket, XSH, argv );
{
char answer[2048];
print_vfs_message( "FISH: Waiting for initial line..." );
if (!get_line(qsockr(bucket), answer, sizeof(answer), ':'))
goto error_2;
print_vfs_message( answer );
if (strstr(answer, "assword")) {
/* Currently, this does not work. ssh reads passwords from
/dev/tty, not from stdin :-(. */
message_1s (1, MSG_ERROR, _("Sorry, we can not do password authenticated connections for now."));
my_errno = EPERM;
goto error_2;
if (!bucket->password){
char *p, *op;
p = copy_strings (" FISH: Password required for ", quser(bucket),
" ", NULL);
op = vfs_get_password (p);
free (p);
my_errno = EPERM;
if (op == NULL)
goto error_2;
bucket->password = strdup (op);
wipe_password(op);
}
print_vfs_message( "FISH: Sending password..." );
write(qsockw(bucket), bucket->password, strlen(bucket->password));
write(qsockw(bucket), "\r\n", 2);
}
}
print_vfs_message( "FISH: Sending initial line..." );
my_errno = ECONNREFUSED;
if (command (bucket, WAIT_REPLY, "#FISH\necho; start_fish_server; echo '### 200'\n") != COMPLETE)
goto error_2;
print_vfs_message( "FISH: Handshaking version..." );
if (command (bucket, WAIT_REPLY, "#VER 0.0.0\necho '### 000'\n") != COMPLETE)
goto error_2;
print_vfs_message( "FISH: Setting up current directory..." );
qhome(bucket) = fish_get_current_directory (bucket);
if (!qhome(bucket))
qhome(bucket) = strdup ("/");
print_vfs_message( "FISH: Connected." );
return bucket;
error_2:
close(qsockr(bucket));
close(qsockw(bucket));
error:
free (qhost(bucket));
free (quser(bucket));
free (bucket);
return NULL;
}
/* This routine keeps track of open connections */
/* Returns a connected socket to host */
static struct connection *
open_link (char *host, char *user, int flags, char *netrcpass)
{
struct connection *bucket;
struct linklist *lptr;
for (lptr = connections_list->next;
lptr != connections_list; lptr = lptr->next) {
bucket = lptr->data;
if ((strcmp (host, qhost(bucket)) == 0) &&
(strcmp (user, quser(bucket)) == 0) &&
(flags == qflags(bucket)))
return bucket;
}
bucket = open_command_connection(host, user, flags, netrcpass);
if (bucket == NULL)
return NULL;
if (!linklist_insert(connections_list, bucket)) {
connection_destructor(bucket);
ERRNOR (ENOMEM, NULL);
}
return bucket;
}
/* The returned directory should always contain a trailing slash */
static char *fish_get_current_directory(struct connection *bucket)
{
if (command(bucket, WANT_STRING, "#PWD\npwd; echo '### 200'\n") == COMPLETE)
return copy_strings(reply_str, "/", NULL);
ERRNOR (EIO, NULL);
}
#define X "fish"
#define X_myname "/#sh:"
#define X_vfs_ops fish_vfs_ops
#define X_fill_names fish_fill_names
#define X_hint_reread fish_hint_reread
#define X_flushdir fish_flushdir
#define X_done fish_done
#include "shared_ftp_fish.c"
static char*
get_path (struct connection **bucket, char *path)
{
char *res;
if (res = s_get_path (bucket, path, "/#sh:"))
return res;
if (res = s_get_path (bucket, path, "/#ssh:"))
return res;
if (res = s_get_path (bucket, path, "/#rsh:")) {
qflags((*bucket)) |= FISH_FLAG_RSH;
return res;
}
return NULL;
}
/*
* This is the 'new' code
*/
/*
* Last parameter (resolve_symlinks) is currently not used. Due to
* the code sharing (file shared_ftp_fish.c) the fish and ftp interface
* have to be the same (Norbert).
*/
static struct dir *
retrieve_dir(struct connection *bucket, char *remote_path, int resolve_symlinks)
{
int has_symlinks;
struct linklist *file_list, *p;
struct direntry *fe;
char buffer[8192];
struct dir *dcache;
int got_intr = 0;
for (p = qdcache(bucket)->next;p != qdcache(bucket);
p = p->next) {
dcache = p->data;
if (strcmp(dcache->remote_path, remote_path) == 0) {
struct timeval tim;
gettimeofday(&tim, NULL);
if ((tim.tv_sec < dcache->timestamp.tv_sec) && !force_expiration)
return dcache;
else {
force_expiration = 0;
p->next->prev = p->prev;
p->prev->next = p->next;
dir_destructor(dcache);
free (p);
break;
}
}
}
has_symlinks = 0;
print_vfs_message("fish: Reading FTP directory...");
my_errno = ENOMEM;
if (!(file_list = linklist_init()))
return NULL;
if (!(dcache = xmalloc(sizeof(struct dir), "struct dir"))) {
linklist_destroy(file_list, NULL);
return NULL;
}
gettimeofday(&dcache->timestamp, NULL);
dcache->timestamp.tv_sec += 360;
dcache->file_list = file_list;
dcache->remote_path = strdup(remote_path);
dcache->count = 1;
command(bucket, NONE,
"#LIST %s\nls -lLa %s | grep '^[^cbt]' | ( while read p x u g s m d y n; do echo \"P$p $u.$g\n"
"S$s\nd$m $d $y\n:$n\n\"; done )\n"
"ls -lLa %s | grep '^[cb]' | ( while read p x u g a i m d y n; do echo \"P$p $u.$g\n"
"E$a$i\nd$m $d $y\n:$n\n\"; done ); echo '### 200'\n",
remote_path, remote_path, remote_path);
/* Clear the interrupt flag */
enable_interrupt_key ();
fe = NULL;
errno = 0;
my_errno = ENOMEM;
while ((got_intr = get_line_interruptible (buffer, sizeof (buffer), qsockr(bucket))) != EINTR){
int eof = (got_intr == 0);
if (logfile){
fputs (buffer, logfile);
fputs ("\n", logfile);
fflush (logfile);
}
if (eof) {
if (fe)
free(fe);
my_errno = ECONNRESET;
goto error_1;
}
if (!strncmp(buffer, "### ", 4))
break;
if ((!buffer[0]) && fe) {
if (!linklist_insert(file_list, fe)) {
free(fe);
goto error_1;
}
fe = NULL;
continue;
}
if (!fe) {
if (!(fe = xmalloc(sizeof(struct direntry), "struct direntry")))
goto error_1;
bzero(fe, sizeof(struct direntry));
fe->count = 1;
fe->bucket = bucket;
fe->s.st_ino = bucket->__inode_counter++;
fe->s.st_nlink = 1;
}
switch(buffer[0]) {
case ':': fe->name = strdup(buffer+1); break;
case 'S': fe->s.st_size = atoi(buffer+1); break;
case 'P': {
int i;
if ((i = parse_filetype(buffer[1])) ==-1)
break;
fe->s.st_mode = i;
if ((i = parse_filemode(buffer+2)) ==-1)
break;
fe->s.st_mode |= i;
}
break;
case 'd': {
split_text(buffer+1);
if (!parse_filedate(0, &fe->s.st_ctime))
break;
fe->s.st_atime = fe->s.st_mtime = fe->s.st_ctime;
}
break;
case 'D': {
struct tm tim;
if (sscanf(buffer+1, "%d %d %d %d %d %d", &tim.tm_year, &tim.tm_mon,
&tim.tm_mday, &tim.tm_hour, &tim.tm_min, &tim.tm_sec) != 6)
break;
fe->s.st_atime = fe->s.st_mtime = fe->s.st_ctime = mktime(&tim);
}
break;
case 'E': {
int maj, min;
if (sscanf(buffer+1, "%d,%d", &maj, &min) != 2)
break;
#ifdef HAVE_ST_RDEV
fe->s.st_rdev = (maj << 8) | min;
#endif
}
case 'L': fe->linkname = strdup(buffer+1);
break;
}
}
disable_interrupt_key();
#if 0
if (got_intr)
vfs_die("fish: reading FTP directory interrupted by user");
#endif
if (decode_reply(buffer+4, 0) != COMPLETE) {
my_errno = EIO;
goto error_3;
}
if (file_list->next == file_list) {
my_errno = EACCES;
goto error_3;
}
if (!linklist_insert(qdcache(bucket), dcache)) {
my_errno = ENOMEM;
goto error_3;
}
print_vfs_message("fish: got listing");
return dcache;
error_1:
disable_interrupt_key();
error_3:
free(dcache->remote_path);
free(dcache);
linklist_destroy(file_list, direntry_destructor);
print_vfs_message("fish: failed");
return NULL;
}
static int
store_file(struct direntry *fe)
{
int local_handle, n, total;
char buffer[8192];
struct stat s;
int was_error = 0;
local_handle = open(fe->local_filename, O_RDONLY);
unlink (fe->local_filename);
my_errno = EIO;
if (local_handle == -1)
return 0;
fstat(local_handle, &s);
/* Use this as stor: ( dd block ; dd malyblock ) | ( cat > file; cat > /dev/null ) */
print_vfs_message("FISH: store: sending command..." );
if (command (fe->bucket, WAIT_REPLY,
"#STOR %d %s\n> %s; echo '### 001'; ( dd bs=4096 count=%d; dd bs=%d count=1 ) 2>/dev/null | ( cat > %s; cat > /dev/null ); echo '### 200'\n",
s.st_size, fe->remote_filename,
fe->remote_filename,
s.st_size / 4096, s.st_size % 4096, fe->remote_filename)
!= PRELIM)
return 0;
total = 0;
enable_interrupt_key();
while (1) {
while ((n = read(local_handle, buffer, sizeof(buffer))) < 0) {
if ((errno == EINTR) && got_interrupt)
continue;
print_vfs_message("FISH: Local read failed, sending zeros" );
close(local_handle);
local_handle = open( "/dev/zero", O_RDONLY );
}
if (n == 0)
break;
while (write(qsockw(fe->bucket), buffer, n) < 0) {
if (errno == EINTR) {
if (got_interrupt()) {
my_errno = EINTR;
goto error_return;
}
else
continue;
}
my_errno = errno;
goto error_return;
}
total += n;
print_vfs_message("fish: storing %s %d (%d)",
was_error ? "zeros" : "file", total, s.st_size);
}
disable_interrupt_key();
close(local_handle);
if (get_reply (qsockr(fe->bucket), NULL, 0) != COMPLETE)
ERRNOR (EIO, 0);
return (!was_error);
error_return:
disable_interrupt_key();
close(local_handle);
get_reply(qsockr(fe->bucket), NULL, 0);
return 0;
}
static int linear_start(struct direntry *fe)
{
fe->local_stat.st_mtime = 0;
if (command(fe->bucket, WANT_STRING,
"#RETR %s\nls -l %s | ( read var1 var2 var3 var4 var5 var6; echo $var5 ); echo '### 100'; cat %s; echo '### 200'\n",
fe->remote_filename, fe->remote_filename, fe->remote_filename )
!= PRELIM) ERRNOR (EACCES, 0);
fe->got = 0;
fe->total = atoi(reply_str);
return 1;
}
static void
linear_abort (struct direntry *fe)
{
char buffer[8192];
int n;
print_vfs_message( "Aborting transfer..." );
do {
n = MIN(8192, fe->total - fe->got);
if (n)
if ((n = read(qsockr(fe->bucket), buffer, n)) < 0)
return;
} while (n);
if (get_reply (qsockr(fe->bucket), NULL, 0) != COMPLETE)
print_vfs_message( "Error reported after abort." );
else
print_vfs_message( "Aborted transfer would be successfull." );
}
static int
linear_read (struct direntry *fe, void *buf, int len)
{
int n = 0;
len = MIN( fe->total - fe->got, len );
while (len && ((n = read (qsockr(fe->bucket), buf, len))<0)) {
if ((errno == EINTR) && !got_interrupt())
continue;
break;
}
if (n>0) fe->got += n;
if (n<0) linear_abort(fe);
if ((!n) && ((get_reply (qsockr (fe->bucket), NULL, 0) != COMPLETE)))
ERRNOR (EIO, -1);
ERRNOR (errno, n);
}
static int
linear_close (struct direntry *fe)
{
if (fe->total != fe->got)
linear_abort(fe);
}
int fish_ctl (void *data, int ctlop, int arg)
{
int n = 0;
struct filp *fp = data;
int v;
switch (ctlop) {
case MCCTL_IS_NOTREADY:
{
int v = select_on_two (qsockr(fp->fe->bucket), 0);
if (((v < 0) && (errno == EINTR)) || v == 0)
return 1;
return 0;
}
default:
return 0;
}
}
static int
send_fish_command(struct connection *bucket, char *cmd, int flags)
{
int r;
int flush_directory_cache = (flags & OPT_FLUSH) && (normal_flush > 0);
r = command (bucket, WAIT_REPLY, cmd);
vfs_add_noncurrent_stamps (&fish_vfs_ops, (vfsid) bucket, NULL);
if (r != COMPLETE) ERRNOR (EPERM, -1);
if (flush_directory_cache)
flush_all_directory(bucket);
return 0;
}
void
fish_init (void)
{
connections_list = linklist_init();
logfile = fopen ("/tmp/talk.fish", "w+");
}
#define PREFIX \
char buf[999]; \
char *remote_path; \
struct connection *bucket; \
if (!(remote_path = get_path(&bucket, path))) \
return -1;
#define POSTFIX(flags) \
free(remote_path); \
return send_fish_command(bucket, buf, flags);
int fish_chmod (char *path, int mode)
{
PREFIX
sprintf(buf, "#CHMOD %4.4o %s\nchmod %4.4o %s; echo '### 000'\n",
mode & 07777, remote_path,
mode & 07777, remote_path);
POSTFIX(OPT_FLUSH);
}
#define FISH_OP(name, chk, string) \
int fish_##name (char *path1, char *path2) \
{ \
char buf[120]; \
char *remote_path1 = NULL, *remote_path2 = NULL; \
struct connection *bucket1, *bucket2; \
if (!(remote_path1 = get_path(&bucket1, path1))) \
return -1; \
if (!(remote_path2 = get_path(&bucket2, path2))) { \
free(remote_path1); \
return -1; \
} \
sprintf(buf, string, path1, path2, path1, path2 ); \
free(remote_path1); \
free(remote_path2); \
return send_fish_command(bucket2, buf, OPT_FLUSH); \
}
#define XTEST if (bucket1 != bucket2) { free(remote_path1); free(remote_path2); ERRNOR (EXDEV, -1); }
FISH_OP(rename, XTEST, "#RENAME %s %s\nmv %s %s; echo '*** 000'" );
FISH_OP(link, XTEST, "#LINK %s %s\nln %s %s; echo '*** 000'" );
FISH_OP(symlink, , "#SYMLINK %s %s\nln -s %s %s; echo '*** 000'" );
int fish_chown (char *path, int owner, int group)
{
char *sowner, *sgroup;
PREFIX
sowner = getpwuid( owner )->pw_name;
sgroup = getgrgid( group )->gr_name;
sprintf(buf, "#CHOWN %s %s\nchown %s %s; echo '### 000'\n",
sowner, remote_path,
sowner, remote_path);
send_fish_command(bucket, buf, OPT_FLUSH);
/* FIXME: what should we report if chgrp succeeds but chown fails? */
sprintf(buf, "#CHGRP %s %s\nchgrp %s %s; echo '### 000'\n",
sgroup, remote_path,
sgroup, remote_path);
free(remote_path);
POSTFIX(OPT_FLUSH)
}
static int fish_unlink (char *path)
{
PREFIX
sprintf(buf, "#DELE %s\nrm -f %s; echo '### 000'\n", remote_path, remote_path);
POSTFIX(OPT_FLUSH);
}
static int fish_mkdir (char *path, mode_t mode)
{
PREFIX
sprintf(buf, "#MKD %s\nmkdir %s; echo '### 000'\n", remote_path, remote_path);
POSTFIX(OPT_FLUSH);
}
static int fish_rmdir (char *path)
{
PREFIX
sprintf(buf, "#RMD %s\nrmdir %s; echo '### 000'\n", remote_path, remote_path);
POSTFIX(OPT_FLUSH);
}
void fish_set_debug (char *file)
{
}
void fish_forget (char *file)
{
#if 0
struct linklist *l;
char *host, *user, *pass, *rp;
int port;
#ifndef BROKEN_PATHS
if (strncmp (file, "/#sh:", 5))
return; /* Normal: consider cd /bla/#ftp */
#else
if (!(file = strstr (file, "/#sh:")))
return;
#endif
file += 6;
if (!(rp = fish_get_host_and_username (file, &host, &user, &port, &pass))) {
free (host);
free (user);
if (pass)
wipe_password (pass);
return;
}
/* we do not care about the path actually */
free (rp);
for (l = connections_list->next; l != connections_list; l = l->next){
struct connection *bucket = l->data;
if ((strcmp (host, qhost (bucket)) == 0) &&
(strcmp (user, quser (bucket)) == 0) &&
(port == qport (bucket))){
/* close socket: the child owns it now */
close (bucket->sock);
bucket->sock = -1;
/* reopen the connection */
bucket->sock = fish_open_socket (bucket);
if (bucket->sock != -1)
login_server (bucket, pass);
break;
}
}
free (host);
free (user);
if (pass)
wipe_password (pass);
#endif
}
vfs fish_vfs_ops = {
s_open,
s_close,
s_read,
s_write,
s_opendir,
s_readdir,
s_closedir,
s_telldir,
s_seekdir,
s_stat,
s_lstat,
s_fstat,
fish_chmod,
fish_chown, /* not really implemented but returns success */
NULL, /* utime */
s_readlink,
fish_symlink, /* symlink */
fish_link, /* link */
fish_unlink,
fish_rename, /* rename */
s_chdir,
s_errno,
s_lseek,
NULL, /* mknod */
s_getid,
s_nothingisopen,
s_free,
s_getlocalcopy,
s_ungetlocalcopy,
fish_mkdir,
fish_rmdir,
fish_ctl,
s_setctl,
fish_forget
#ifdef HAVE_MMAP
, NULL,
NULL
#endif
};