/* * Copyright (c) 1999-2005 Sendmail, Inc. and its suppliers. * All rights reserved. * * By using this file, you agree to the terms and conditions set * forth in the LICENSE file which can be found at the top level of * the sendmail distribution. * * Contributed by Exactis.com, Inc. * */ #include "sm/generic.h" SM_RCSID("@(#)$Id: bf.c,v 1.22 2006/07/16 02:07:39 ca Exp $") #include "sm/types.h" #include "sm/stat.h" #include "sm/uio.h" #include "sm/fcntl.h" #include "sm/error.h" #include "sm/rpool.h" #include "sm/memops.h" #include "sm/io.h" #include "sm/bf.h" #include "sm/debug.h" /* ** Notice: this does double buffering: ** once in sm_file_T, once here. We should get rid of one layer. ** Possible solutions: ** 1. get rid of the one here, but: does it work? ** "All" this should do is avoiding creating/deleting the file ** until the available buffer size is exceeded. Currently there is ** too much copying going on. ** 2. disable the sm I/O buffer by using sm_io_setvbuf() ** done, see below. */ /* bf io functions */ static open_F sm_bfopen; static close_F sm_bfclose; static read_F sm_bfread; static write_F sm_bfwrite; static seek_F sm_bfseek; static setinfo_F sm_bfsetinfo; static getinfo_F sm_bfgetinfo; sm_stream_T SmBfIO = SM_STREAM_STRUCT(sm_bfopen, sm_bfclose, sm_bfread, sm_bfwrite, NULL, NULL, sm_bfseek, sm_bfgetinfo, sm_bfsetinfo); /* Data structure for storing information about each buffered file */ struct bf { bool bf_committed; /* Has this buffered file been committed? */ bool bf_ondisk; /* On disk: committed or buffer overflow */ int bf_disk_fd; /* If on disk, associated file descriptor */ /* this should be a generic (I/O) buffer */ uchar *bf_buf; /* Memory buffer */ int bf_bufsize; /* Length of above buffer */ int bf_buffilled; /* Bytes of buffer actually filled */ char *bf_filename; /* Name of buffered file, if ever committed */ mode_t bf_filemode; /* Mode of buffered file, if ever committed */ off_t bf_offset; /* Currect file offset */ int bf_size; /* Total current size of file */ }; #define OPEN(fn, omode, cmode) open(fn, omode, cmode) /* ** SM_BFOPEN -- the "base" open function called by sm_io_open() for the ** internal, file-type-specific info setup. ** ** Parameters: ** fp -- file pointer being filled-in for file being open'd ** info -- information about file being opened ** flags -- ignored ** ** Returns: ** usual sm_error code */ static sm_ret_T sm_bfopen(sm_file_T *fp, const void *info, int flags, va_list ap) { const char *filename; mode_t fmode; size_t bsize; struct bf *bfp; int l; size_t s; struct stat st; SM_IS_FP(fp); SM_REQUIRE(info != NULL); filename = (const char *) info; fmode = 0600; bsize = SM_IO_BUFSIZ; /* Sanity checks */ if (*filename == '\0') { /* Empty filename string */ return sm_error_perm(SM_EM_IO, ENOENT); } if (stat(filename, &st) == 0) { /* File already exists on disk */ return sm_error_perm(SM_EM_IO, EEXIST); } /* Allocate memory */ bfp = (struct bf *) sm_malloc(sizeof(struct bf)); if (bfp == NULL) return sm_error_temp(SM_EM_IO, ENOMEM); for (;;) { l = va_arg(ap, int); if (l == SM_IO_WHAT_END) break; switch (l) { case SM_IO_WHAT_BF_BUFSIZE: bsize = va_arg(ap, size_t); break; case SM_IO_WHAT_FMODE: fmode = (mode_t) va_arg(ap, int); break; default: /* ignore unknown values? */ /* what should we do about the argument then? */ (void *)va_arg(ap, int); break; } } /* Assign data buffer */ /* A zero bsize is valid, just don't allocate memory */ if (bsize > 0) { bfp->bf_buf = (uchar *) sm_malloc(bsize); if (bfp->bf_buf == NULL) { bfp->bf_bufsize = 0; sm_free(bfp); return sm_error_temp(SM_EM_IO, ENOMEM); } /* turn off buffering in upper layer */ l = sm_io_setvbuf(fp, NULL, SM_IO_NBF, 0); SM_ASSERT(SM_SUCCESS == l); } else bfp->bf_buf = NULL; /* Nearly home free, just set all the parameters now */ bfp->bf_committed = false; bfp->bf_ondisk = false; bfp->bf_bufsize = bsize; bfp->bf_buffilled = 0; s = strlen(filename) + 1; bfp->bf_filename = (char *) sm_malloc(s); if (bfp->bf_filename == NULL) { SM_FREE(bfp->bf_buf); sm_free(bfp); return sm_error_temp(SM_EM_IO, ENOMEM); } (void) strlcpy(bfp->bf_filename, filename, s); bfp->bf_filemode = fmode; bfp->bf_offset = 0; bfp->bf_size = 0; bfp->bf_disk_fd = -1; f_cookie(*fp) = bfp; return SM_SUCCESS; } /* ** SM_BFGETINFO -- returns info about an open file pointer ** ** Parameters: ** fp -- file pointer to get info about ** what -- type of info to obtain ** valp -- thing to return the info in */ static sm_ret_T sm_bfgetinfo(sm_file_T *fp, int what, void *valp) { struct bf *bfp; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); switch (what) { case SM_IO_WHAT_BF_BUFSIZE: return bfp->bf_bufsize; case SM_IO_WHAT_FD: return bfp->bf_disk_fd; case SM_IO_WHAT_SIZE: return bfp->bf_size; default: return sm_error_perm(SM_EM_IO, EINVAL); } } /* ** SM_BFCLOSE -- close a buffered file ** ** Parameters: ** fp -- cookie of file to close ** flags -- ignored ** ** Returns: ** usual sm_error code ** ** Side Effects: ** deletes backing file, sm_frees memory. */ static int sm_bfclose(sm_file_T *fp, int flags) { struct bf *bfp; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); /* Need to clean up the file */ if (bfp->bf_ondisk && !bfp->bf_committed) unlink(bfp->bf_filename); sm_free(bfp->bf_filename); if (bfp->bf_disk_fd != -1) close(bfp->bf_disk_fd); SM_FREE(bfp->bf_buf); /* Finally, sm_free the structure */ sm_free(bfp); return SM_SUCCESS; } /* ** SM_BFREAD -- read a buffered file ** ** Parameters: ** cookie -- cookie of file to read ** buf -- buffer to fill ** nbytes -- how many bytes to read ** bytesread -- number of bytes read (output) ** ** Returns: ** number of bytes read or -1 indicate failure ** ** Side Effects: ** none. ** */ static sm_ret_T sm_bfread(sm_file_T *fp, uchar *buf, size_t nbytes, ssize_t *bytesread) { struct bf *bfp; ssize_t count; /* Number of bytes put in buf so far */ int retval; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); count = 0; if (bfp->bf_offset < bfp->bf_buffilled) { /* Need to grab some from buffer */ count = nbytes; if ((bfp->bf_offset + count) > bfp->bf_buffilled) count = bfp->bf_buffilled - bfp->bf_offset; sm_memcpy(buf, bfp->bf_buf + bfp->bf_offset, count); } if ((bfp->bf_offset + nbytes) > bfp->bf_buffilled) { /* Need to grab some from file */ if (!bfp->bf_ondisk) { /* Oops, the file doesn't exist. EOF. */ goto finished; } /* Catch a read() on an earlier failed write to disk */ if (bfp->bf_disk_fd < 0) return sm_error_perm(SM_EM_IO, EIO); if (lseek(bfp->bf_disk_fd, bfp->bf_offset + count, SEEK_SET) < 0) { if (errno == EINVAL || errno == ESPIPE) errno = EIO; return sm_error_perm(SM_EM_IO, errno); } while (count < nbytes) { retval = read(bfp->bf_disk_fd, buf + count, nbytes - count); if (retval < 0) return sm_error_perm(SM_EM_IO, errno); else if (retval == 0) goto finished; else count += retval; } } finished: bfp->bf_offset += count; *bytesread = count; return SM_SUCCESS; } /* ** SM_BFSEEK -- seek to a position in a buffered file ** ** Parameters: ** fp -- fp of file to seek ** offset -- position to seek to ** whence -- how to seek ** ** Returns: ** new file offset or -1 indicate failure ** */ static sm_ret_T sm_bfseek(sm_file_T *fp, off_t offset, int whence) { struct bf *bfp; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); switch (whence) { case SEEK_SET: bfp->bf_offset = offset; break; case SEEK_CUR: bfp->bf_offset += offset; break; case SEEK_END: bfp->bf_offset = bfp->bf_size + offset; break; default: return sm_error_perm(SM_EM_IO, EINVAL); } return bfp->bf_offset; } /* ** SM_BFWRITE -- write to a buffered file ** ** Parameters: ** fp -- fp of file to write ** buf -- data buffer ** nbytes -- how many bytes to write ** byteswritten -- number of bytes written (output) ** ** Returns: ** number of bytes written or -1 indicate failure ** ** Side Effects: ** may create backing file if over memory limit for file. ** */ static sm_ret_T sm_bfwrite(sm_file_T *fp, const uchar *buf, size_t nbytes, ssize_t *byteswritten) { struct bf *bfp; ssize_t count; /* Number of bytes written so far */ int retval; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); count = 0; /* If committed, go straight to disk */ if (bfp->bf_committed) { if (lseek(bfp->bf_disk_fd, bfp->bf_offset, SEEK_SET) < 0) { if (errno == EINVAL || errno == ESPIPE) errno = EIO; return sm_error_perm(SM_EM_IO, errno); } count = write(bfp->bf_disk_fd, buf, nbytes); if (count < 0) return sm_error_perm(SM_EM_IO, errno); goto finished; } if (bfp->bf_offset < bfp->bf_bufsize) { /* Need to put some in buffer */ count = nbytes; if ((bfp->bf_offset + count) > bfp->bf_bufsize) count = bfp->bf_bufsize - bfp->bf_offset; sm_memcpy(bfp->bf_buf + bfp->bf_offset, buf, count); if ((bfp->bf_offset + count) > bfp->bf_buffilled) bfp->bf_buffilled = bfp->bf_offset + count; } if ((bfp->bf_offset + nbytes) > bfp->bf_bufsize) { /* Need to put some in file */ if (!bfp->bf_ondisk) { mode_t omask; /* Clear umask as bf_filemode are the true perms */ omask = umask(0); retval = OPEN(bfp->bf_filename, O_RDWR | O_CREAT | O_TRUNC, bfp->bf_filemode); (void) umask(omask); /* Couldn't create file: failure */ if (retval < 0) { /* ** stdio may not be expecting these ** errnos from write()! Change to ** something which it can understand. ** Note that ENOSPC and EDQUOT are saved ** because they are actually valid for ** write(). */ if (!(errno == ENOSPC #ifdef EDQUOT || errno == EDQUOT #endif )) errno = EIO; return sm_error_perm(SM_EM_IO, errno); } bfp->bf_disk_fd = retval; bfp->bf_ondisk = true; } /* Catch a write() on an earlier failed write to disk */ if (bfp->bf_ondisk && bfp->bf_disk_fd < 0) return sm_error_perm(SM_EM_IO, EIO); if (lseek(bfp->bf_disk_fd, bfp->bf_offset + count, SEEK_SET) < 0) { if (errno == EINVAL || errno == ESPIPE) errno = EIO; return sm_error_perm(SM_EM_IO, errno); } while (count < nbytes) { retval = write(bfp->bf_disk_fd, buf + count, nbytes - count); if (retval < 0) return sm_error_perm(SM_EM_IO, errno); else count += retval; } } finished: bfp->bf_offset += count; if (bfp->bf_offset > bfp->bf_size) bfp->bf_size = bfp->bf_offset; *byteswritten = count; return SM_SUCCESS; } /* ** BFREWIND -- rewinds the sm_file_T * ** ** Parameters: ** fp -- sm_file_T * to rewind ** ** Returns: ** usual sm_error code ** ** Side Effects: ** rewinds the sm_file_T * and puts it into read mode. Normally one ** would bfopen() a file, write to it, then bfrewind() and ** fread(). If fp is not a buffered file, this is equivalent to ** rewind(). */ static sm_ret_T bfrewind(sm_file_T *fp) { SM_IS_FP(fp); (void) sm_io_flush(fp); sm_io_clearerr(fp); /* quicker just to do it */ return sm_io_seek(fp, 0L, SM_IO_SEEK_SET); } /* ** SM_BFCOMMIT -- "commits" the buffered file ** ** Parameters: ** fp -- sm_file_T * to commit to disk ** sync -- invoke fsync()? ** ** Returns: ** usual sm_error code ** ** Side Effects: ** Forces the given sm_file_T * to be written to disk if it is not ** already, and ensures that it will be kept after closing. If ** fp is not a buffered file, this is a no-op. */ static sm_ret_T sm_bfcommit(sm_file_T *fp, bool sync) { struct bf *bfp; int retval; int byteswritten; sm_ret_T ret; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); /* If already committed, noop */ if (bfp->bf_committed) return SM_SUCCESS; /* Do we need to open a file? */ if (!bfp->bf_ondisk) { mode_t omask; struct stat st; if (stat(bfp->bf_filename, &st) == 0) return sm_error_perm(SM_EM_IO, EEXIST); /* Clear umask as bf_filemode are the true perms */ omask = umask(0); retval = OPEN(bfp->bf_filename, O_RDWR | O_CREAT | O_TRUNC, bfp->bf_filemode); (void) umask(omask); /* Couldn't create file: failure */ if (retval < 0) return sm_error_perm(SM_EM_IO, errno); bfp->bf_disk_fd = retval; bfp->bf_ondisk = true; } /* Write out the contents of our buffer, if we have any */ if (bfp->bf_buffilled > 0) { byteswritten = 0; if (lseek(bfp->bf_disk_fd, (off_t) 0, SEEK_SET) < 0) return sm_error_perm(SM_EM_IO, errno); while (byteswritten < bfp->bf_buffilled) { retval = write(bfp->bf_disk_fd, bfp->bf_buf + byteswritten, bfp->bf_buffilled - byteswritten); if (retval < 0) return sm_error_perm(SM_EM_IO, errno); else byteswritten += retval; } } bfp->bf_committed = true; ret = SM_SUCCESS; if (sync) { SM_ASSERT(is_valid_fd(bfp->bf_disk_fd)); if (fsync(bfp->bf_disk_fd) == -1) ret = sm_error_perm(SM_EM_IO, errno); } /* Invalidate buf; all goes to file now */ bfp->bf_buffilled = 0; if (bfp->bf_bufsize > 0) { sm_io_setvbuf(fp, bfp->bf_buf, SM_IO_FBF, bfp->bf_bufsize); bfp->bf_bufsize = 0; } return ret; } /* ** SM_BFTRUNCATE -- rewinds and truncates the sm_file_T * ** ** Parameters: ** fp -- sm_file_T * to truncate ** ** Returns: ** usual sm_error code ** ** Side Effects: ** rewinds the sm_file_T *, truncates it to zero length, and puts ** it into write mode. */ static sm_ret_T sm_bftruncate(sm_file_T *fp) { sm_ret_T ret; struct bf *bfp; SM_IS_FP(fp); ret = bfrewind(fp); if (sm_is_err(ret)) return ret; /* Get bf structure */ bfp = (struct bf *) f_cookie(*fp); bfp->bf_buffilled = 0; bfp->bf_size = 0; /* Need to zero the buffer */ if (bfp->bf_buf != NULL) sm_memzero(bfp->bf_buf, bfp->bf_bufsize); if (bfp->bf_ondisk) { #if NOFTRUNCATE /* XXX: Not much we can do except rewind it */ return sm_error_perm(SM_EM_IO, EINVAL); #else if (ftruncate(bfp->bf_disk_fd, (off_t) 0) < 0) return sm_error_perm(SM_EM_IO, errno); #endif } return SM_SUCCESS; } /* ** SM_BFSETINFO -- set/change info for an open file pointer ** ** Parameters: ** fp -- file pointer to get info about ** what -- type of info to set/change ** valp -- thing to set/change the info to ** */ static sm_ret_T sm_bfsetinfo(sm_file_T *fp, int what, void *valp) { struct bf *bfp; int bsize; SM_IS_FP(fp); bfp = (struct bf *) f_cookie(*fp); switch (what) { case SM_IO_WHAT_BF_BUFSIZE: bsize = *((int *) valp); bfp->bf_bufsize = bsize; /* A zero bsize is valid, just don't allocate memory */ if (bsize > 0) { bfp->bf_buf = (uchar *) sm_malloc(bsize); if (bfp->bf_buf == NULL) { bfp->bf_bufsize = 0; return sm_error_temp(SM_EM_IO, ENOMEM); } } else bfp->bf_buf = NULL; return SM_SUCCESS; case SM_IO_WHAT_COMMIT: return sm_bfcommit(fp, true); case SM_IO_WHAT_BF_COMMIT: return sm_bfcommit(fp, false); case SM_IO_WHAT_BF_TRUNCATE: return sm_bftruncate(fp); case SM_IO_WHAT_BF_TEST: return SM_SUCCESS; /* always */ default: return sm_error_perm(SM_EM_IO, EINVAL); } /* NOTREACHED */ }