/* * Trivial File Transfer Protocol (RFC 1350) * * Transfer file to/from remote host * * W. Eric Norum * Saskatchewan Accelerator Laboratory * University of Saskatchewan * Saskatoon, Saskatchewan, CANADA * eric@skatter.usask.ca * * Modifications to support reference counting in the file system are * Copyright (c) 2012 embedded brains GmbH. * * $Id$ * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef RTEMS_TFTP_DRIVER_DEBUG int rtems_tftp_driver_debug = 1; #endif /* * Range of UDP ports to try */ #define UDP_PORT_BASE 3180 /* * Default limits */ #define PACKET_FIRST_TIMEOUT_MILLISECONDS 400L #define PACKET_TIMEOUT_MILLISECONDS 6000L #define OPEN_RETRY_LIMIT 10 #define IO_RETRY_LIMIT 10 /* * TFTP opcodes */ #define TFTP_OPCODE_RRQ 1 #define TFTP_OPCODE_WRQ 2 #define TFTP_OPCODE_DATA 3 #define TFTP_OPCODE_ACK 4 #define TFTP_OPCODE_ERROR 5 /* * Largest data transfer */ #define TFTP_BUFSIZE 512 /* * Packets transferred between machines */ union tftpPacket { /* * RRQ/WRQ packet */ struct tftpRWRQ { uint16_t opcode; char filename_mode[TFTP_BUFSIZE]; } tftpRWRQ; /* * DATA packet */ struct tftpDATA { uint16_t opcode; uint16_t blocknum; uint8_t data[TFTP_BUFSIZE]; } tftpDATA; /* * ACK packet */ struct tftpACK { uint16_t opcode; uint16_t blocknum; } tftpACK; /* * ERROR packet */ struct tftpERROR { uint16_t opcode; uint16_t errorCode; char errorMessage[TFTP_BUFSIZE]; } tftpERROR; }; /* * State of each TFTP stream */ struct tftpStream { /* * Buffer for storing most recently-received packet */ union tftpPacket pkbuf; /* * Last block number transferred */ uint16_t blocknum; /* * Data transfer socket */ int socket; struct sockaddr_in myAddress; struct sockaddr_in farAddress; /* * Indices into buffer */ int nleft; int nused; /* * Flags */ int firstReply; int eof; int writing; }; /* * Flags for filesystem info. */ #define TFTPFS_VERBOSE (1 << 0) /* * TFTP File system info. */ typedef struct tftpfs_info_s { uint32_t flags; rtems_id tftp_mutex; int nStreams; struct tftpStream ** volatile tftpStreams; } tftpfs_info_t; #define tftpfs_info_mount_table(_mt) ((tftpfs_info_t*) ((_mt)->fs_info)) #define tftpfs_info_pathloc(_pl) ((tftpfs_info_t*) ((_pl)->mt_entry->fs_info)) #define tftpfs_info_iop(_iop) (tftpfs_info_pathloc (&((_iop)->pathinfo))) /* * Number of streams open at the same time */ static const rtems_filesystem_operations_table rtems_tftp_ops; static const rtems_filesystem_file_handlers_r rtems_tftp_handlers; static bool rtems_tftp_is_directory( const char *path, size_t pathlen ) { return path [pathlen - 1] == '/'; } int rtems_tftpfs_initialize( rtems_filesystem_mount_table_entry_t *mt_entry, const void *data ) { rtems_status_code sc; const char *device = mt_entry->dev; size_t devicelen = strlen (device); tftpfs_info_t *fs; char *root_path; if (devicelen == 0) rtems_set_errno_and_return_minus_one (ENXIO); fs = malloc (sizeof (*fs)); root_path = malloc (devicelen + 2); if (root_path == NULL || fs == NULL) goto error; root_path = memcpy (root_path, device, devicelen); root_path [devicelen] = '/'; root_path [devicelen + 1] = '\0'; fs->flags = 0; fs->nStreams = 0; fs->tftpStreams = 0; mt_entry->fs_info = fs; mt_entry->mt_fs_root->location.node_access = root_path; mt_entry->mt_fs_root->location.handlers = &rtems_tftp_handlers; mt_entry->mt_fs_root->location.ops = &rtems_tftp_ops; /* * Now allocate a semaphore for mutual exclusion. * * NOTE: This could be in an fsinfo for this filesystem type. */ sc = rtems_semaphore_create ( rtems_build_name('T', 'F', 'T', 'P'), 1, RTEMS_FIFO | RTEMS_BINARY_SEMAPHORE | RTEMS_NO_INHERIT_PRIORITY | RTEMS_NO_PRIORITY_CEILING | RTEMS_LOCAL, 0, &fs->tftp_mutex ); if (sc != RTEMS_SUCCESSFUL) goto error; if (data) { char* config = (char*) data; char* token; char* saveptr; token = strtok_r (config, " ", &saveptr); while (token) { if (strcmp (token, "verbose") == 0) fs->flags |= TFTPFS_VERBOSE; token = strtok_r (NULL, " ", &saveptr); } } return 0; error: free (fs); free (root_path); rtems_set_errno_and_return_minus_one (ENOMEM); } /* * Release a stream and clear the pointer to it */ static void releaseStream (tftpfs_info_t *fs, int s) { if (fs->tftpStreams[s] && (fs->tftpStreams[s]->socket >= 0)) close (fs->tftpStreams[s]->socket); rtems_semaphore_obtain (fs->tftp_mutex, RTEMS_WAIT, RTEMS_NO_TIMEOUT); free (fs->tftpStreams[s]); fs->tftpStreams[s] = NULL; rtems_semaphore_release (fs->tftp_mutex); } static void rtems_tftpfs_shutdown (rtems_filesystem_mount_table_entry_t* mt_entry) { tftpfs_info_t *fs = tftpfs_info_mount_table (mt_entry); int s; for (s = 0; s < fs->nStreams; s++) releaseStream (fs, s); rtems_semaphore_delete (fs->tftp_mutex); free (fs); free (mt_entry->mt_fs_root->location.node_access); } /* * Map error message */ static int tftpErrno (struct tftpStream *tp) { unsigned int tftpError; static const int errorMap[] = { EINVAL, ENOENT, EPERM, ENOSPC, EINVAL, ENXIO, EEXIST, ESRCH, }; tftpError = ntohs (tp->pkbuf.tftpERROR.errorCode); if (tftpError < (sizeof errorMap / sizeof errorMap[0])) return errorMap[tftpError]; else return 1000 + tftpError; } /* * Send a message to make the other end shut up */ static void sendStifle (struct tftpStream *tp, struct sockaddr_in *to) { int len; struct { uint16_t opcode; uint16_t errorCode; char errorMessage[12]; } msg; /* * Create the error packet (Unknown transfer ID). */ msg.opcode = htons (TFTP_OPCODE_ERROR); msg.errorCode = htons (5); len = sizeof msg.opcode + sizeof msg.errorCode + 1; len += sprintf (msg.errorMessage, "GO AWAY"); /* * Send it */ sendto (tp->socket, (char *)&msg, len, 0, (struct sockaddr *)to, sizeof *to); } /* * Wait for a data packet */ static int getPacket (struct tftpStream *tp, int retryCount) { int len; struct timeval tv; if (retryCount == 0) { tv.tv_sec = PACKET_FIRST_TIMEOUT_MILLISECONDS / 1000L; tv.tv_usec = (PACKET_FIRST_TIMEOUT_MILLISECONDS % 1000L) * 1000L; } else { tv.tv_sec = PACKET_TIMEOUT_MILLISECONDS / 1000L; tv.tv_usec = (PACKET_TIMEOUT_MILLISECONDS % 1000L) * 1000L; } setsockopt (tp->socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); for (;;) { union { struct sockaddr s; struct sockaddr_in i; } from; socklen_t fromlen = sizeof from; len = recvfrom (tp->socket, &tp->pkbuf, sizeof tp->pkbuf, 0, &from.s, &fromlen); if (len < 0) break; if (from.i.sin_addr.s_addr == tp->farAddress.sin_addr.s_addr) { if (tp->firstReply) { tp->firstReply = 0; tp->farAddress.sin_port = from.i.sin_port; } if (tp->farAddress.sin_port == from.i.sin_port) break; } /* * Packet is from someone with whom we are * not interested. Tell them to go away. */ sendStifle (tp, &from.i); } tv.tv_sec = 0; tv.tv_usec = 0; setsockopt (tp->socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); #ifdef RTEMS_TFTP_DRIVER_DEBUG if (rtems_tftp_driver_debug) { if (len >= (int) sizeof tp->pkbuf.tftpACK) { int opcode = ntohs (tp->pkbuf.tftpDATA.opcode); switch (opcode) { default: printf ("TFTP: OPCODE %d\n", opcode); break; case TFTP_OPCODE_DATA: printf ("TFTP: RECV %d\n", ntohs (tp->pkbuf.tftpDATA.blocknum)); break; case TFTP_OPCODE_ACK: printf ("TFTP: GOT ACK %d\n", ntohs (tp->pkbuf.tftpACK.blocknum)); break; } } else { printf ("TFTP: %d-byte packet\n", len); } } #endif return len; } /* * Send an acknowledgement */ static int sendAck (struct tftpStream *tp) { #ifdef RTEMS_TFTP_DRIVER_DEBUG if (rtems_tftp_driver_debug) printf ("TFTP: ACK %d\n", tp->blocknum); #endif /* * Create the acknowledgement */ tp->pkbuf.tftpACK.opcode = htons (TFTP_OPCODE_ACK); tp->pkbuf.tftpACK.blocknum = htons (tp->blocknum); /* * Send it */ if (sendto (tp->socket, (char *)&tp->pkbuf, sizeof tp->pkbuf.tftpACK, 0, (struct sockaddr *)&tp->farAddress, sizeof tp->farAddress) < 0) return errno; return 0; } /* * Convert a path to canonical form */ static void fixPath (char *path) { char *inp, *outp, *base; outp = inp = path; base = NULL; for (;;) { if (inp[0] == '.') { if (inp[1] == '\0') break; if (inp[1] == '/') { inp += 2; continue; } if (inp[1] == '.') { if (inp[2] == '\0') { if ((base != NULL) && (outp > base)) { outp--; while ((outp > base) && (outp[-1] != '/')) outp--; } break; } if (inp[2] == '/') { inp += 3; if (base == NULL) continue; if (outp > base) { outp--; while ((outp > base) && (outp[-1] != '/')) outp--; } continue; } } } if (base == NULL) base = inp; while (inp[0] != '/') { if ((*outp++ = *inp++) == '\0') return; } *outp++ = '/'; while (inp[0] == '/') inp++; } *outp = '\0'; return; } static void rtems_tftp_eval_path(rtems_filesystem_eval_path_context_t *self) { int eval_flags = rtems_filesystem_eval_path_get_flags (self); if ((eval_flags & RTEMS_LIBIO_MAKE) == 0) { int rw = RTEMS_LIBIO_PERMS_READ | RTEMS_LIBIO_PERMS_WRITE; if ((eval_flags & rw) != rw) { rtems_filesystem_location_info_t *currentloc = rtems_filesystem_eval_path_get_currentloc (self); char *current = currentloc->node_access; size_t currentlen = strlen (current); const char *path = rtems_filesystem_eval_path_get_path (self); size_t pathlen = rtems_filesystem_eval_path_get_pathlen (self); size_t len = currentlen + pathlen; rtems_filesystem_eval_path_clear_path (self); current = realloc (current, len + 1); if (current != NULL) { memcpy (current + currentlen, path, pathlen); current [len] = '\0'; if (!rtems_tftp_is_directory (current, len)) { fixPath (current); } currentloc->node_access = current; } else { rtems_filesystem_eval_path_error (self, ENOMEM); } } else { rtems_filesystem_eval_path_error (self, EINVAL); } } else { rtems_filesystem_eval_path_error (self, EIO); } } /* * The routine which does most of the work for the IMFS open handler */ static int rtems_tftp_open_worker( rtems_libio_t *iop, char *full_path_name, int oflag ) { tftpfs_info_t *fs; struct tftpStream *tp; int retryCount; struct in_addr farAddress; int s; int len; char *cp1; char *cp2; char *remoteFilename; rtems_interval now; rtems_status_code sc; char *hostname; /* * Get the file system info. */ fs = tftpfs_info_iop (iop); /* * Extract the host name component */ hostname = full_path_name; cp1 = strchr (full_path_name, ':'); if (!cp1) hostname = "BOOTP_HOST"; else { *cp1 = '\0'; ++cp1; } /* * Convert hostname to Internet address */ if (strcmp (hostname, "BOOTP_HOST") == 0) farAddress = rtems_bsdnet_bootp_server_address; else if (inet_aton (hostname, &farAddress) == 0) { struct hostent *he = gethostbyname(hostname); if (he == NULL) return ENOENT; memcpy (&farAddress, he->h_addr, sizeof (farAddress)); } /* * Extract file pathname component */ if (strcmp (cp1, "BOOTP_FILE") == 0) { cp1 = rtems_bsdnet_bootp_boot_file_name; } if (*cp1 == '\0') return ENOENT; remoteFilename = cp1; if (strlen (remoteFilename) > (TFTP_BUFSIZE - 10)) return ENOENT; /* * Find a free stream */ sc = rtems_semaphore_obtain (fs->tftp_mutex, RTEMS_WAIT, RTEMS_NO_TIMEOUT); if (sc != RTEMS_SUCCESSFUL) return EBUSY; for (s = 0 ; s < fs->nStreams ; s++) { if (fs->tftpStreams[s] == NULL) break; } if (s == fs->nStreams) { /* * Reallocate stream pointers * Guard against the case where realloc() returns NULL. */ struct tftpStream **np; np = realloc (fs->tftpStreams, ++fs->nStreams * sizeof *fs->tftpStreams); if (np == NULL) { rtems_semaphore_release (fs->tftp_mutex); return ENOMEM; } fs->tftpStreams = np; } tp = fs->tftpStreams[s] = malloc (sizeof (struct tftpStream)); rtems_semaphore_release (fs->tftp_mutex); if (tp == NULL) return ENOMEM; iop->data0 = s; iop->data1 = tp; /* * Create the socket */ if ((tp->socket = socket (AF_INET, SOCK_DGRAM, 0)) < 0) { releaseStream (fs, s); return ENOMEM; } /* * Bind the socket to a local address */ retryCount = 0; now = rtems_clock_get_ticks_since_boot(); for (;;) { int try = (now + retryCount) % 10; tp->myAddress.sin_family = AF_INET; tp->myAddress.sin_port = htons (UDP_PORT_BASE + fs->nStreams * try + s); tp->myAddress.sin_addr.s_addr = htonl (INADDR_ANY); if (bind (tp->socket, (struct sockaddr *)&tp->myAddress, sizeof tp->myAddress) >= 0) break; if (++retryCount == 10) { releaseStream (fs, s); return EBUSY; } } /* * Set the UDP destination to the TFTP server * port on the remote machine. */ tp->farAddress.sin_family = AF_INET; tp->farAddress.sin_addr = farAddress; tp->farAddress.sin_port = htons (69); /* * Start the transfer */ tp->firstReply = 1; retryCount = 0; for (;;) { /* * Create the request */ if ((oflag & O_ACCMODE) == O_RDONLY) { tp->writing = 0; tp->pkbuf.tftpRWRQ.opcode = htons (TFTP_OPCODE_RRQ); } else { tp->writing = 1; tp->pkbuf.tftpRWRQ.opcode = htons (TFTP_OPCODE_WRQ); } cp1 = (char *) tp->pkbuf.tftpRWRQ.filename_mode; cp2 = (char *) remoteFilename; while ((*cp1++ = *cp2++) != '\0') continue; cp2 = "octet"; while ((*cp1++ = *cp2++) != '\0') continue; len = cp1 - (char *)&tp->pkbuf.tftpRWRQ; /* * Send the request */ if (sendto (tp->socket, (char *)&tp->pkbuf, len, 0, (struct sockaddr *)&tp->farAddress, sizeof tp->farAddress) < 0) { releaseStream (fs, s); return EIO; } /* * Get reply */ len = getPacket (tp, retryCount); if (len >= (int) sizeof tp->pkbuf.tftpACK) { int opcode = ntohs (tp->pkbuf.tftpDATA.opcode); if (!tp->writing && (opcode == TFTP_OPCODE_DATA) && (ntohs (tp->pkbuf.tftpDATA.blocknum) == 1)) { tp->nused = 0; tp->blocknum = 1; tp->nleft = len - 2 * sizeof (uint16_t ); tp->eof = (tp->nleft < TFTP_BUFSIZE); if (sendAck (tp) != 0) { releaseStream (fs, s); return EIO; } break; } if (tp->writing && (opcode == TFTP_OPCODE_ACK) && (ntohs (tp->pkbuf.tftpACK.blocknum) == 0)) { tp->nused = 0; tp->blocknum = 1; break; } if (opcode == TFTP_OPCODE_ERROR) { int e = tftpErrno (tp); releaseStream (fs, s); return e; } } /* * Keep trying */ if (++retryCount >= OPEN_RETRY_LIMIT) { releaseStream (fs, s); return EIO; } } return 0; } static int rtems_tftp_open( rtems_libio_t *iop, const char *new_name, int oflag, mode_t mode ) { tftpfs_info_t *fs; char *full_path_name; int err; full_path_name = iop->pathinfo.node_access; if (rtems_tftp_is_directory (full_path_name, strlen (full_path_name))) { rtems_set_errno_and_return_minus_one (ENOTSUP); } /* * Get the file system info. */ fs = tftpfs_info_iop (iop); if (fs->flags & TFTPFS_VERBOSE) printf ("TFTPFS: %s\n", full_path_name); err = rtems_tftp_open_worker (iop, full_path_name, oflag); if (err != 0) { rtems_set_errno_and_return_minus_one (err); } return 0; } /* * Read from a TFTP stream */ static ssize_t rtems_tftp_read( rtems_libio_t *iop, void *buffer, size_t count ) { char *bp; struct tftpStream *tp = iop->data1; int retryCount; int nwant; if (!tp) rtems_set_errno_and_return_minus_one( EIO ); /* * Read till user request is satisfied or EOF is reached */ bp = buffer; nwant = count; while (nwant) { if (tp->nleft) { int ncopy; if (nwant < tp->nleft) ncopy = nwant; else ncopy = tp->nleft; memcpy (bp, &tp->pkbuf.tftpDATA.data[tp->nused], ncopy); tp->nused += ncopy; tp->nleft -= ncopy; bp += ncopy; nwant -= ncopy; if (nwant == 0) break; } if (tp->eof) break; /* * Wait for the next packet */ retryCount = 0; for (;;) { int len = getPacket (tp, retryCount); if (len >= (int)sizeof tp->pkbuf.tftpACK) { int opcode = ntohs (tp->pkbuf.tftpDATA.opcode); uint16_t nextBlock = tp->blocknum + 1; if ((opcode == TFTP_OPCODE_DATA) && (ntohs (tp->pkbuf.tftpDATA.blocknum) == nextBlock)) { tp->nused = 0; tp->nleft = len - 2 * sizeof (uint16_t); tp->eof = (tp->nleft < TFTP_BUFSIZE); tp->blocknum++; if (sendAck (tp) != 0) rtems_set_errno_and_return_minus_one (EIO); break; } if (opcode == TFTP_OPCODE_ERROR) rtems_set_errno_and_return_minus_one (tftpErrno (tp)); } /* * Keep trying? */ if (++retryCount == IO_RETRY_LIMIT) rtems_set_errno_and_return_minus_one (EIO); if (sendAck (tp) != 0) rtems_set_errno_and_return_minus_one (EIO); } } return count - nwant; } /* * Flush a write buffer and wait for acknowledgement */ static int rtems_tftp_flush (struct tftpStream *tp) { int wlen, rlen; int retryCount = 0; wlen = tp->nused + 2 * sizeof (uint16_t ); for (;;) { tp->pkbuf.tftpDATA.opcode = htons (TFTP_OPCODE_DATA); tp->pkbuf.tftpDATA.blocknum = htons (tp->blocknum); #ifdef RTEMS_TFTP_DRIVER_DEBUG if (rtems_tftp_driver_debug) printf ("TFTP: SEND %d (%d)\n", tp->blocknum, tp->nused); #endif if (sendto (tp->socket, (char *)&tp->pkbuf, wlen, 0, (struct sockaddr *)&tp->farAddress, sizeof tp->farAddress) < 0) return EIO; rlen = getPacket (tp, retryCount); /* * Our last packet won't necessarily be acknowledged! */ if ((rlen < 0) && (tp->nused < sizeof tp->pkbuf.tftpDATA.data)) return 0; if (rlen >= (int)sizeof tp->pkbuf.tftpACK) { int opcode = ntohs (tp->pkbuf.tftpACK.opcode); if ((opcode == TFTP_OPCODE_ACK) && (ntohs (tp->pkbuf.tftpACK.blocknum) == tp->blocknum)) { tp->nused = 0; tp->blocknum++; return 0; } if (opcode == TFTP_OPCODE_ERROR) return tftpErrno (tp); } /* * Keep trying? */ if (++retryCount == IO_RETRY_LIMIT) return EIO; } } /* * Close a TFTP stream */ static int rtems_tftp_close( rtems_libio_t *iop ) { tftpfs_info_t *fs; struct tftpStream *tp = iop->data1; int e = 0; /* * Get the file system info. */ fs = tftpfs_info_iop (iop); if (!tp) rtems_set_errno_and_return_minus_one (EIO); if (tp->writing) e = rtems_tftp_flush (tp); if (!tp->eof && !tp->firstReply) { /* * Tell the other end to stop */ rtems_interval ticksPerSecond; sendStifle (tp, &tp->farAddress); ticksPerSecond = rtems_clock_get_ticks_per_second(); rtems_task_wake_after (1 + ticksPerSecond / 10); } releaseStream (fs, iop->data0); rtems_set_errno_and_return_minus_one (e); } static ssize_t rtems_tftp_write( rtems_libio_t *iop, const void *buffer, size_t count ) { const char *bp; struct tftpStream *tp = iop->data1; int nleft, nfree, ncopy; /* * Bail out if an error has occurred */ if (!tp->writing) rtems_set_errno_and_return_minus_one (EIO); /* * Write till user request is satisfied * Notice that the buffer is flushed as soon as it is filled rather * than waiting for the next write or a close. This ensures that * the flush in close writes a less than full buffer so the far * end can detect the end-of-file condition. */ bp = buffer; nleft = count; while (nleft) { nfree = sizeof tp->pkbuf.tftpDATA.data - tp->nused; if (nleft < nfree) ncopy = nleft; else ncopy = nfree; memcpy (&tp->pkbuf.tftpDATA.data[tp->nused], bp, ncopy); tp->nused += ncopy; nleft -= ncopy; bp += ncopy; if (tp->nused == sizeof tp->pkbuf.tftpDATA.data) { int e = rtems_tftp_flush (tp); if (e) { tp->writing = 0; rtems_set_errno_and_return_minus_one (e); } } } return count; } /* * Dummy version to let fopen(xxxx,"w") work properly. */ static int rtems_tftp_ftruncate( rtems_libio_t *iop __attribute__((unused)), off_t count __attribute__((unused)) ) { return 0; } static rtems_filesystem_node_types_t rtems_tftp_node_type( const rtems_filesystem_location_info_t *loc ) { const char *path = loc->node_access; size_t pathlen = strlen (path); return rtems_tftp_is_directory (path, pathlen) ? RTEMS_FILESYSTEM_DIRECTORY : RTEMS_FILESYSTEM_MEMORY_FILE; } static int rtems_tftp_clone( rtems_filesystem_location_info_t *loc ) { int rv = 0; loc->node_access = strdup (loc->node_access); if (loc->node_access == NULL) { errno = ENOMEM; rv = -1; } return rv; } static void rtems_tftp_free_node_info( const rtems_filesystem_location_info_t *loc ) { free (loc->node_access); } static bool rtems_tftp_are_nodes_equal( const rtems_filesystem_location_info_t *a, const rtems_filesystem_location_info_t *b ) { return strcmp (a->node_access, b->node_access) == 0; } static const rtems_filesystem_operations_table rtems_tftp_ops = { .lock_h = rtems_filesystem_default_lock, .unlock_h = rtems_filesystem_default_unlock, .eval_path_h = rtems_tftp_eval_path, .link_h = rtems_filesystem_default_link, .are_nodes_equal_h = rtems_tftp_are_nodes_equal, .node_type_h = rtems_tftp_node_type, .mknod_h = rtems_filesystem_default_mknod, .rmnod_h = rtems_filesystem_default_rmnod, .fchmod_h = rtems_filesystem_default_fchmod, .chown_h = rtems_filesystem_default_chown, .clonenod_h = rtems_tftp_clone, .freenod_h = rtems_tftp_free_node_info, .mount_h = rtems_filesystem_default_mount, .fsmount_me_h = rtems_tftpfs_initialize, .unmount_h = rtems_filesystem_default_unmount, .fsunmount_me_h = rtems_tftpfs_shutdown, .utime_h = rtems_filesystem_default_utime, .symlink_h = rtems_filesystem_default_symlink, .readlink_h = rtems_filesystem_default_readlink, .rename_h = rtems_filesystem_default_rename, .statvfs_h = rtems_filesystem_default_statvfs }; static const rtems_filesystem_file_handlers_r rtems_tftp_handlers = { .open_h = rtems_tftp_open, .close_h = rtems_tftp_close, .read_h = rtems_tftp_read, .write_h = rtems_tftp_write, .ioctl_h = rtems_filesystem_default_ioctl, .lseek_h = rtems_filesystem_default_lseek, .fstat_h = rtems_filesystem_default_fstat, .ftruncate_h = rtems_tftp_ftruncate, .fsync_h = rtems_filesystem_default_fsync, .fdatasync_h = rtems_filesystem_default_fdatasync, .fcntl_h = rtems_filesystem_default_fcntl };